diff --git a/app/Console/Commands/CancelStalePayments.php b/app/Console/Commands/CancelStalePayments.php new file mode 100644 index 0000000..00dfb00 --- /dev/null +++ b/app/Console/Commands/CancelStalePayments.php @@ -0,0 +1,34 @@ +subHours(MiniPayment::STALE_PENDING_HOURS); + + $count = MiniPayment::query() + ->where('status', MiniPayment::STATUS_PENDING) + ->where('created_at', '<', $cutoff) + ->update([ + 'status' => MiniPayment::STATUS_CANCELED, + 'updated_at' => now(), + ]); + + $this->info("Canceled {$count} stale pending payment(s)."); + + return self::SUCCESS; + } +} diff --git a/app/Http/Controllers/Api/Mini/AccountController.php b/app/Http/Controllers/Api/Mini/AccountController.php index 9121159..f2a45cc 100644 --- a/app/Http/Controllers/Api/Mini/AccountController.php +++ b/app/Http/Controllers/Api/Mini/AccountController.php @@ -7,6 +7,7 @@ use App\Http\Controllers\Controller; use App\Models\QrSetting; use Illuminate\Http\JsonResponse; use Illuminate\Http\Request; +use Illuminate\Support\Facades\Http; class AccountController extends Controller { @@ -89,6 +90,34 @@ class AccountController extends Controller ]); } + public function uploadAvatar(Request $request): JsonResponse + { + $account = ladill_account(); + + $request->validate([ + 'avatar' => ['required', 'image', 'mimes:jpg,jpeg,png,webp', 'max:4096'], + ]); + + $file = $request->file('avatar'); + + $response = Http::baseUrl(rtrim((string) config('services.ladill_identity.url'), '/')) + ->withToken((string) config('services.ladill_identity.key')) + ->acceptJson() + ->attach('avatar', (string) file_get_contents($file->getRealPath()), $file->getClientOriginalName()) + ->post('/api/identity/avatar', ['user' => $account->public_id]); + + $this->rethrowValidation($response, 'avatar'); + + if ($response->successful()) { + $picture = (string) $response->json('data.user.picture', ''); + if ($picture !== '') { + $account->update(['avatar_url' => $picture]); + } + } + + return response()->json(['data' => ['avatar_url' => $account->fresh()->avatarUrl()]]); + } + public function changePassword(Request $request): JsonResponse { $account = ladill_account(); diff --git a/app/Http/Controllers/Api/Mini/WalletController.php b/app/Http/Controllers/Api/Mini/WalletController.php index 360fc23..5caefe6 100644 --- a/app/Http/Controllers/Api/Mini/WalletController.php +++ b/app/Http/Controllers/Api/Mini/WalletController.php @@ -62,4 +62,54 @@ class WalletController extends Controller return response()->json(['data' => ['checkout_url' => $checkoutUrl]]); } + + public function payoutAccount(): JsonResponse + { + $response = $this->identitySend('GET', '/api/identity/payout-account?user='.urlencode((string) ladill_account()->public_id), []); + + return response()->json(['data' => ['payout_account' => $response->json('data.payout_account')]]); + } + + public function updatePayoutAccount(Request $request): JsonResponse + { + $data = $request->validate([ + 'account_type' => ['required', 'in:mobile_money,bank_account'], + 'account_name' => ['required', 'string', 'max:200'], + 'account_number' => ['required', 'string', 'max:30'], + 'bank_code' => ['required', 'string', 'max:30'], + 'bank_name' => ['required', 'string', 'max:200'], + 'currency' => ['sometimes', 'string', 'size:3'], + ]); + $data['currency'] = $data['currency'] ?? 'GHS'; + + $response = $this->identitySend('PUT', '/api/identity/payout-account', array_merge( + ['user' => ladill_account()->public_id], + $data, + )); + $this->rethrowValidation($response, 'account_number'); + + return response()->json(['data' => ['payout_account' => $response->json('data.payout_account')]]); + } + + public function withdrawals(): JsonResponse + { + $response = $this->identitySend('GET', '/api/identity/wallet/withdrawals?user='.urlencode((string) ladill_account()->public_id), []); + + return response()->json(['data' => $response->json('data', [])]); + } + + public function withdraw(Request $request): JsonResponse + { + $data = $request->validate([ + 'amount' => ['required', 'numeric', 'min:1', 'max:50000'], + ]); + + $response = $this->identitySend('POST', '/api/identity/wallet/withdraw', [ + 'user' => ladill_account()->public_id, + 'amount' => (float) $data['amount'], + ]); + $this->rethrowValidation($response, 'amount'); + + return response()->json(['data' => $response->json('data', [])]); + } } diff --git a/app/Models/MiniPayment.php b/app/Models/MiniPayment.php index 166e14e..53f646f 100644 --- a/app/Models/MiniPayment.php +++ b/app/Models/MiniPayment.php @@ -10,6 +10,10 @@ class MiniPayment extends Model public const STATUS_PENDING = 'pending'; public const STATUS_PAID = 'paid'; public const STATUS_FAILED = 'failed'; + public const STATUS_CANCELED = 'canceled'; + + /** Pending payments are auto-canceled after this many hours. */ + public const STALE_PENDING_HOURS = 6; /** Platform fee on trader payments (Ladill Mini tier). */ public const PLATFORM_FEE_RATE = 0.05; diff --git a/routes/api.php b/routes/api.php index dab5b69..b9e11b5 100644 --- a/routes/api.php +++ b/routes/api.php @@ -45,10 +45,15 @@ Route::prefix('v1')->group(function () { Route::get('/mini/account/settings', [MiniAccountController::class, 'settings'])->name('api.mini.account.settings'); Route::put('/mini/account/settings', [MiniAccountController::class, 'updateSettings'])->name('api.mini.account.settings.update'); Route::put('/mini/account/profile', [MiniAccountController::class, 'updateProfile'])->name('api.mini.account.profile'); + Route::post('/mini/account/avatar', [MiniAccountController::class, 'uploadAvatar'])->name('api.mini.account.avatar'); Route::post('/mini/account/change-password', [MiniAccountController::class, 'changePassword'])->name('api.mini.account.change-password'); Route::get('/mini/wallet', [MiniWalletController::class, 'show'])->name('api.mini.wallet'); Route::post('/mini/wallet/topup', [MiniWalletController::class, 'topup'])->name('api.mini.wallet.topup'); + Route::get('/mini/wallet/payout-account', [MiniWalletController::class, 'payoutAccount'])->name('api.mini.wallet.payout-account'); + Route::put('/mini/wallet/payout-account', [MiniWalletController::class, 'updatePayoutAccount'])->name('api.mini.wallet.payout-account.update'); + Route::get('/mini/wallet/withdrawals', [MiniWalletController::class, 'withdrawals'])->name('api.mini.wallet.withdrawals'); + Route::post('/mini/wallet/withdraw', [MiniWalletController::class, 'withdraw'])->name('api.mini.wallet.withdraw'); Route::post('/mini/support/chat', [MiniSupportController::class, 'chat'])->name('api.mini.support.chat'); }); diff --git a/routes/console.php b/routes/console.php index 3aede45..7a016ba 100644 --- a/routes/console.php +++ b/routes/console.php @@ -8,6 +8,8 @@ Artisan::command('inspire', function () { $this->comment(Inspiring::quote()); })->purpose('Display an inspiring quote'); +Schedule::command('mini:cancel-stale-payments')->hourly()->withoutOverlapping(); + Schedule::command('hosting:check-node-health --all')->everyFiveMinutes()->withoutOverlapping(); Schedule::command('hosting:sync-account-usage')->hourly()->withoutOverlapping(); Schedule::command('hosting:recalculate-node-capacity --shared-only')->daily()->withoutOverlapping();