Add Ladill POS v1 — register, Pay checkout, and commerce links.
Deploy Ladill Mini / deploy (push) Successful in 23s

Staff-facing counter register at pos.ladill.com with catalog cart, cash and
MoMo/card checkout via Ladill Pay, CRM timeline/import, invoice prefill, and
Merchant catalog import.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
isaacclad
2026-06-23 22:52:24 +00:00
co-authored by Cursor
commit e5d2b84388
378 changed files with 41419 additions and 0 deletions
@@ -0,0 +1,156 @@
<?php
namespace App\Http\Controllers\Api\Mini;
use App\Http\Controllers\Api\Concerns\CallsIdentityApi;
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
{
use CallsIdentityApi;
public function settings(): JsonResponse
{
$account = ladill_account();
$settings = $account->getOrCreateQrSetting();
// Phone lives on the central account, not the local mirror — fetch it,
// but never let a transient identity-API hiccup break settings loading.
$phone = '';
$phoneCc = '';
try {
$profile = $this->identitySend('GET', '/api/identity/profile?user='.urlencode((string) $account->public_id), []);
if ($profile->successful()) {
$phone = (string) $profile->json('data.phone', '');
$phoneCc = (string) $profile->json('data.phone_cc', '');
}
} catch (\Throwable) {
// leave phone blank
}
return response()->json([
'data' => [
'profile' => [
'name' => $account->name,
'email' => $account->email,
'phone' => $phone,
'phone_cc' => $phoneCc,
],
'notifications' => [
'notify_email' => $settings->notify_email ?: $account->email,
'product_updates' => (bool) ($settings->product_updates ?? true),
'notify_registrations' => (bool) ($settings->notify_registrations ?? true),
'notify_payouts' => (bool) ($settings->notify_payouts ?? true),
],
],
]);
}
public function updateSettings(Request $request): JsonResponse
{
$account = ladill_account();
$data = $request->validate([
'notify_email' => ['nullable', 'email', 'max:255'],
'product_updates' => ['sometimes', 'boolean'],
'notify_registrations' => ['sometimes', 'boolean'],
'notify_payouts' => ['sometimes', 'boolean'],
]);
$settings = QrSetting::updateOrCreate(
['user_id' => $account->id],
[
'notify_email' => $data['notify_email'] ?? $account->email,
'product_updates' => $request->boolean('product_updates'),
'notify_registrations' => $request->boolean('notify_registrations'),
'notify_payouts' => $request->boolean('notify_payouts'),
],
);
return response()->json([
'data' => [
'notify_email' => $settings->notify_email,
'product_updates' => (bool) $settings->product_updates,
'notify_registrations' => (bool) $settings->notify_registrations,
'notify_payouts' => (bool) $settings->notify_payouts,
],
]);
}
public function updateProfile(Request $request): JsonResponse
{
$account = ladill_account();
$data = $request->validate([
'name' => ['required', 'string', 'max:255'],
'phone_cc' => ['nullable', 'string', 'max:8'],
'phone' => ['nullable', 'string', 'max:32'],
]);
$response = $this->identitySend('PUT', '/api/identity/profile', array_merge(
['user' => $account->public_id],
$data,
));
$this->rethrowValidation($response, 'name');
if ($response->successful()) {
$account->update(['name' => $data['name']]);
}
return response()->json([
'data' => ['name' => $account->fresh()->name, 'email' => $account->email],
]);
}
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();
$request->validate([
'current_password' => ['required', 'string'],
'password' => ['required', 'string', 'min:8', 'confirmed'],
]);
$response = $this->identitySend('POST', '/api/identity/auth/change-password', [
'user' => $account->public_id,
'current_password' => $request->string('current_password'),
'password' => $request->string('password'),
'password_confirmation' => $request->string('password_confirmation'),
]);
$this->rethrowValidation($response, 'current_password');
return response()->json(['data' => ['message' => 'Password updated.']]);
}
}
@@ -0,0 +1,82 @@
<?php
namespace App\Http\Controllers\Api\Mini;
use App\Http\Controllers\Controller;
use App\Models\QrCode;
use App\Services\Afia\AfiaService;
use App\Services\Billing\BillingClient;
use App\Support\Qr\QrTypeCatalog;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
class AfiaController extends Controller
{
public function chat(Request $request, AfiaService $afia): JsonResponse
{
$validated = $request->validate([
'message' => ['required', 'string', 'max:2000'],
'history' => ['nullable', 'array', 'max:20'],
'history.*.role' => ['nullable', 'string'],
'history.*.text' => ['nullable', 'string'],
]);
if (! $afia->enabled()) {
return response()->json(['message' => 'Afia is not available right now.'], 503);
}
try {
$reply = $afia->chat(trim($validated['message']), $validated['history'] ?? [], $this->context());
} catch (\Throwable $e) {
report($e);
return response()->json(['message' => 'Afia could not respond right now. Please try again.'], 502);
}
return response()->json(['reply' => $reply]);
}
/** @return array<string,mixed> */
private function context(): array
{
$account = ladill_account();
$types = QrTypeCatalog::paymentTypes();
$codes = $account->qrCodes()->whereIn('type', $types);
$ctx = [
'signed_in' => 'yes',
'qr_codes_total' => (clone $codes)->count(),
'qr_codes_active' => (clone $codes)->where('is_active', true)->count(),
'scans_total' => (int) (clone $codes)->sum('scans_total'),
'top_code_type' => (clone $codes)
->selectRaw('type, count(*) as total')
->groupBy('type')
->orderByDesc('total')
->value('type') ?: 'none yet',
];
if ($account->public_id) {
try {
$ctx['wallet_balance_ghs'] = number_format(app(BillingClient::class)->balanceMinor((string) $account->public_id) / 100, 2);
} catch (\Throwable) {
}
}
$recent = $account->qrCodes()
->whereIn('type', $types)
->latest('updated_at')
->limit(3)
->get(['type', 'label', 'short_code', 'scans_total']);
if ($recent->isNotEmpty()) {
$ctx['recent_codes'] = $recent->map(fn (QrCode $code): string => sprintf(
'%s (%s, %d scans)',
$code->label ?: $code->short_code,
QrTypeCatalog::label($code->type),
$code->scans_total,
))->implode('; ');
}
return $ctx;
}
}
@@ -0,0 +1,76 @@
<?php
namespace App\Http\Controllers\Api\Mini;
use App\Http\Controllers\Controller;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
class NotificationController extends Controller
{
public function index(Request $request): JsonResponse
{
$account = ladill_account();
$notifications = $account->notifications()
->latest()
->limit(100)
->get()
->map(fn ($n) => $this->present($n));
$unread = $account->unreadNotifications()->count();
return response()->json([
'data' => $notifications,
'unread_count' => $unread,
]);
}
public function unreadCount(Request $request): JsonResponse
{
$account = ladill_account();
return response()->json([
'data' => [
'unread_count' => $account->unreadNotifications()->count(),
],
]);
}
public function markAsRead(Request $request, string $id): JsonResponse
{
$notification = ladill_account()
->notifications()
->where('id', $id)
->first();
if ($notification) {
$notification->markAsRead();
}
return response()->json(['data' => ['success' => true]]);
}
public function markAllAsRead(Request $request): JsonResponse
{
ladill_account()->unreadNotifications->markAsRead();
return response()->json(['data' => ['success' => true]]);
}
/** @return array<string, mixed> */
private function present(mixed $notification): array
{
return [
'id' => $notification->id,
'type' => class_basename($notification->type),
'title' => $notification->data['title'] ?? 'Notification',
'message' => $notification->data['message'] ?? '',
'icon' => $notification->data['icon'] ?? 'bell',
'milestone' => $notification->data['milestone'] ?? null,
'url' => $notification->data['url'] ?? null,
'read_at' => $notification->read_at?->toIso8601String(),
'created_at' => $notification->created_at?->toIso8601String(),
];
}
}
@@ -0,0 +1,59 @@
<?php
namespace App\Http\Controllers\Api\Mini;
use App\Http\Controllers\Controller;
use App\Models\MiniPayment;
use App\Models\QrCode;
use App\Services\Billing\BillingClient;
use Illuminate\Http\JsonResponse;
use Illuminate\Support\Facades\Log;
use Throwable;
class OverviewController extends Controller
{
public function __construct(private BillingClient $billing) {}
public function __invoke(): JsonResponse
{
$account = ladill_account();
$qrIds = $account->qrCodes()
->where('type', QrCode::TYPE_PAYMENT)
->pluck('id');
$todayPayments = MiniPayment::query()
->whereIn('qr_code_id', $qrIds)
->where('status', MiniPayment::STATUS_PAID)
->where('paid_at', '>=', now()->startOfDay());
$recentPayments = MiniPayment::query()
->whereIn('qr_code_id', $qrIds)
->where('status', MiniPayment::STATUS_PAID)
->with('qrCode')
->latest('paid_at')
->limit(8)
->get();
$balanceMinor = 0;
try {
$balanceMinor = $this->billing->balanceMinor($account->public_id);
} catch (Throwable $e) {
Log::warning('Mini API overview could not load wallet balance', [
'user' => $account->public_id,
'error' => $e->getMessage(),
]);
}
return response()->json([
'data' => [
'currency' => 'GHS',
'today_takings_minor' => (int) (clone $todayPayments)->sum('merchant_amount_minor'),
'today_count' => (clone $todayPayments)->count(),
'payment_qr_count' => $qrIds->count(),
'wallet_balance_minor' => $balanceMinor,
'recent_payments' => $recentPayments->map(fn (MiniPayment $p) => PaymentPresenter::present($p))->values(),
],
]);
}
}
@@ -0,0 +1,56 @@
<?php
namespace App\Http\Controllers\Api\Mini;
use App\Models\MiniPayment;
use App\Models\QrCode;
/**
* Shared JSON shape for Mini payments and payment QRs so the Android app and
* the web views stay describing the same records.
*/
class PaymentPresenter
{
/** @return array<string, mixed> */
public static function present(MiniPayment $payment): array
{
return [
'id' => $payment->id,
'reference' => $payment->reference,
'amount_minor' => $payment->amount_minor,
'merchant_amount_minor' => $payment->merchant_amount_minor,
'platform_fee_minor' => $payment->platform_fee_minor,
'currency' => $payment->currency,
'status' => $payment->status,
'payer_name' => $payment->payer_name,
'payer_email' => $payment->payer_email,
'payer_phone' => $payment->payer_phone,
'payer_note' => $payment->payer_note,
'qr_code_id' => $payment->qr_code_id,
'qr_label' => $payment->qrCode?->label,
'paid_at' => $payment->paid_at?->toIso8601String(),
'created_at' => $payment->created_at?->toIso8601String(),
];
}
/** @return array<string, mixed> */
public static function presentQr(QrCode $qr, ?string $previewUrl = null): array
{
$content = $qr->content();
return [
'id' => $qr->id,
'label' => $qr->label,
'business_name' => $content['business_name'] ?? null,
'branch_label' => $content['branch_label'] ?? null,
'currency' => $content['currency'] ?? 'GHS',
'short_code' => $qr->short_code,
'public_url' => $qr->publicUrl(),
'is_active' => $qr->is_active,
'scans_total' => $qr->scans_total,
'preview_url' => $previewUrl,
'created_at' => $qr->created_at?->toIso8601String(),
'updated_at' => $qr->updated_at?->toIso8601String(),
];
}
}
@@ -0,0 +1,131 @@
<?php
namespace App\Http\Controllers\Api\Mini;
use App\Http\Controllers\Controller;
use App\Models\QrCode;
use App\Services\Qr\QrCodeManagerService;
use App\Services\Qr\QrImageGeneratorService;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use RuntimeException;
use Symfony\Component\HttpFoundation\Response;
class PaymentQrController extends Controller
{
public function __construct(
private QrCodeManagerService $manager,
private QrImageGeneratorService $imageGenerator,
) {}
public function index(): JsonResponse
{
$qrCodes = ladill_account()->qrCodes()
->where('type', QrCode::TYPE_PAYMENT)
->latest()
->get();
return response()->json([
'data' => $qrCodes->map(fn (QrCode $qr) => PaymentPresenter::presentQr($qr, $this->previewUrl($qr)))->values(),
]);
}
public function store(Request $request): JsonResponse
{
abort_unless($request->user()->tokenCan('mini:write'), 403, 'Token requires mini:write ability.');
$data = $request->validate([
'label' => ['required', 'string', 'max:120'],
'business_name' => ['required', 'string', 'max:120'],
'branch_label' => ['nullable', 'string', 'max:80'],
]);
$data['type'] = QrCode::TYPE_PAYMENT;
$data['currency'] = 'GHS';
try {
$qrCode = $this->manager->create(ladill_account(), $data);
} catch (RuntimeException $e) {
return response()->json(['message' => $e->getMessage()], 422);
}
return response()->json([
'data' => PaymentPresenter::presentQr($qrCode->fresh(), $this->previewUrl($qrCode)),
], 201);
}
public function show(QrCode $paymentQr): JsonResponse
{
$this->authorizePaymentQr($paymentQr);
return response()->json([
'data' => PaymentPresenter::presentQr($paymentQr, $this->previewUrl($paymentQr)),
]);
}
public function update(Request $request, QrCode $paymentQr): JsonResponse
{
abort_unless($request->user()->tokenCan('mini:write'), 403, 'Token requires mini:write ability.');
$this->authorizePaymentQr($paymentQr);
$data = $request->validate([
'label' => ['sometimes', 'string', 'max:120'],
'business_name' => ['sometimes', 'string', 'max:120'],
'branch_label' => ['nullable', 'string', 'max:80'],
'is_active' => ['sometimes', 'boolean'],
]);
if ($request->has('is_active')) {
$data['is_active'] = $request->boolean('is_active');
}
try {
$this->manager->update($paymentQr, $data);
} catch (RuntimeException $e) {
return response()->json(['message' => $e->getMessage()], 422);
}
return response()->json([
'data' => PaymentPresenter::presentQr($paymentQr->fresh(), $this->previewUrl($paymentQr)),
]);
}
public function destroy(QrCode $paymentQr): JsonResponse
{
abort_unless(request()->user()->tokenCan('mini:write'), 403, 'Token requires mini:write ability.');
$this->authorizePaymentQr($paymentQr);
$this->manager->delete($paymentQr);
return response()->json(['data' => ['message' => 'Payment QR deleted.']]);
}
public function preview(QrCode $paymentQr): Response
{
$this->authorizePaymentQr($paymentQr);
$qrCode = $this->imageGenerator->ensureValidImages($paymentQr);
$bytes = $this->imageGenerator->normalizeStoredPng($qrCode->png_path);
if ($bytes === null) {
$bytes = $this->imageGenerator->renderPng($qrCode->encodedPayload(), $qrCode->style());
$this->imageGenerator->generateAndStore($qrCode);
}
return response($bytes, 200, [
'Content-Type' => 'image/png',
'Cache-Control' => 'private, max-age=3600',
]);
}
private function previewUrl(QrCode $qr): string
{
return route('api.mini.payment-qrs.preview', $qr);
}
private function authorizePaymentQr(QrCode $paymentQr): void
{
abort_unless($paymentQr->type === QrCode::TYPE_PAYMENT, 404);
abort_unless($paymentQr->user_id === ladill_account()->id, 403);
}
}
@@ -0,0 +1,49 @@
<?php
namespace App\Http\Controllers\Api\Mini;
use App\Http\Controllers\Controller;
use App\Models\MiniPayment;
use App\Models\QrCode;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
class PaymentsController extends Controller
{
public function index(Request $request): JsonResponse
{
$search = trim((string) $request->query('q', ''));
$qrIds = ladill_account()->qrCodes()
->where('type', QrCode::TYPE_PAYMENT)
->pluck('id');
$payments = MiniPayment::query()
->whereIn('qr_code_id', $qrIds)
->when($search !== '', function ($query) use ($search) {
$like = '%'.$search.'%';
$query->where(function ($inner) use ($like) {
$inner->where('payer_name', 'like', $like)
->orWhere('payer_email', 'like', $like)
->orWhere('payer_note', 'like', $like)
->orWhere('reference', 'like', $like)
->orWhere('payment_reference', 'like', $like)
->orWhereHas('qrCode', fn ($qr) => $qr->where('label', 'like', $like));
});
})
->with('qrCode')
->latest('created_at')
->paginate(25)
->withQueryString();
return response()->json([
'data' => collect($payments->items())->map(fn (MiniPayment $p) => PaymentPresenter::present($p))->values(),
'meta' => [
'current_page' => $payments->currentPage(),
'last_page' => $payments->lastPage(),
'per_page' => $payments->perPage(),
'total' => $payments->total(),
],
]);
}
}
@@ -0,0 +1,49 @@
<?php
namespace App\Http\Controllers\Api\Mini;
use App\Http\Controllers\Controller;
use App\Models\MiniPayment;
use App\Models\QrCode;
use App\Services\Billing\BillingClient;
use Illuminate\Http\JsonResponse;
use Illuminate\Support\Facades\Log;
use Throwable;
class PayoutsController extends Controller
{
public function __construct(private BillingClient $billing) {}
public function __invoke(): JsonResponse
{
$account = ladill_account();
$qrIds = $account->qrCodes()
->where('type', QrCode::TYPE_PAYMENT)
->pluck('id');
$revenueMinor = (int) MiniPayment::query()
->whereIn('qr_code_id', $qrIds)
->where('status', MiniPayment::STATUS_PAID)
->sum('merchant_amount_minor');
$balanceMinor = 0;
try {
$balanceMinor = $this->billing->balanceMinor($account->public_id);
} catch (Throwable $e) {
Log::warning('Mini API payouts could not load wallet balance', [
'user' => $account->public_id,
'error' => $e->getMessage(),
]);
}
return response()->json([
'data' => [
'currency' => 'GHS',
'total_revenue_minor' => $revenueMinor,
'wallet_balance_minor' => $balanceMinor,
'account_wallet_url' => 'https://'.config('app.account_domain').'/wallet',
],
]);
}
}
@@ -0,0 +1,51 @@
<?php
namespace App\Http\Controllers\Api\Mini;
use App\Http\Controllers\Controller;
use App\Models\UserPushToken;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
class PushTokenController extends Controller
{
public function store(Request $request): JsonResponse
{
$data = $request->validate([
'token' => ['required', 'string', 'max:512'],
'platform' => ['nullable', 'string', 'max:32'],
'device_name' => ['nullable', 'string', 'max:120'],
]);
$user = $request->user();
$now = now();
UserPushToken::updateOrCreate(
['token' => $data['token']],
[
'user_id' => $user->id,
'platform' => $data['platform'] ?? 'android',
'device_name' => $data['device_name'] ?? $user->currentAccessToken()?->name,
'last_seen_at' => $now,
],
);
$user->update(['last_app_active_at' => $now]);
return response()->json(['data' => ['registered' => true]]);
}
public function destroy(Request $request): JsonResponse
{
$data = $request->validate([
'token' => ['required', 'string', 'max:512'],
]);
$request->user()
->pushTokens()
->where('token', $data['token'])
->delete();
return response()->json(['data' => ['removed' => true]]);
}
}
@@ -0,0 +1,56 @@
<?php
namespace App\Http\Controllers\Api\Mini;
use App\Http\Controllers\Api\Concerns\CallsIdentityApi;
use App\Http\Controllers\Controller;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
/**
* Support tickets proxies to the central support system (auth.ladill.com),
* so tickets land in the same admin support queue as every other Ladill app.
*/
class SupportController extends Controller
{
use CallsIdentityApi;
public function tickets(): JsonResponse
{
$response = $this->identitySend('GET', '/api/identity/support/tickets?user='.urlencode((string) ladill_account()->public_id), []);
return response()->json(['data' => $response->json('data', [])]);
}
public function ticket(int $ticket): JsonResponse
{
$response = $this->identitySend(
'GET',
'/api/identity/support/tickets/'.$ticket.'?user='.urlencode((string) ladill_account()->public_id),
[],
);
if ($response->status() === 404) {
return response()->json(['message' => 'Ticket not found.'], 404);
}
return response()->json(['data' => $response->json('data')]);
}
public function store(Request $request): JsonResponse
{
$data = $request->validate([
'subject' => ['required', 'string', 'max:255'],
'message' => ['required', 'string', 'max:5000'],
'priority' => ['sometimes', 'in:low,normal,high'],
]);
$response = $this->identitySend('POST', '/api/identity/support/tickets', array_merge(
['user' => ladill_account()->public_id],
$data,
));
$this->rethrowValidation($response, 'subject');
return response()->json(['data' => $response->json('data')], 201);
}
}
@@ -0,0 +1,161 @@
<?php
namespace App\Http\Controllers\Api\Mini;
use App\Http\Controllers\Api\Concerns\CallsIdentityApi;
use App\Http\Controllers\Controller;
use App\Services\Billing\BillingClient;
use App\Services\Mini\AutoWithdrawService;
use App\Services\Notifications\MiniNotificationService;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Log;
use Throwable;
class WalletController extends Controller
{
use CallsIdentityApi;
public function __construct(
private BillingClient $billing,
private MiniNotificationService $notifications,
private AutoWithdrawService $autoWithdraw,
) {}
public function show(): JsonResponse
{
$account = ladill_account();
$balanceMinor = 0;
$ledger = [];
try {
$balanceMinor = $this->billing->balanceMinor($account->public_id);
$ledger = $this->billing->serviceLedger($account->public_id, config('billing.service', 'mini'));
} catch (Throwable $e) {
Log::warning('Mini API wallet load failed', ['user' => $account->public_id, 'error' => $e->getMessage()]);
}
$settings = $account->getOrCreateQrSetting();
return response()->json([
'data' => [
'currency' => 'GHS',
'balance_minor' => $balanceMinor,
'spent_minor' => (int) ($ledger['spent_minor'] ?? 0),
'credited_minor' => (int) ($ledger['credited_minor'] ?? 0),
'auto_withdraw_amount_minor' => $settings->auto_withdraw_amount_minor,
],
]);
}
public function updateAutoWithdraw(Request $request): JsonResponse
{
$account = ladill_account();
$data = $request->validate([
'amount' => ['nullable', 'numeric', 'min:1', 'max:50000'],
]);
$amountMinor = isset($data['amount']) ? (int) round((float) $data['amount'] * 100) : null;
$settings = $account->getOrCreateQrSetting();
$settings->update(['auto_withdraw_amount_minor' => $amountMinor]);
if ($amountMinor !== null) {
$this->autoWithdraw->attemptForUser($account);
}
return response()->json([
'data' => [
'auto_withdraw_amount_minor' => $settings->fresh()->auto_withdraw_amount_minor,
],
]);
}
public function topup(Request $request): JsonResponse
{
$account = ladill_account();
$data = $request->validate([
'amount' => ['required', 'numeric', 'min:1', 'max:10000'],
]);
$response = $this->identitySend('POST', '/api/identity/wallet-topup-account', [
'user' => $account->public_id,
'amount' => (float) $data['amount'],
// Paystack returns the customer here after payment; the app catches the deep link.
'return_url' => 'https://'.config('app.platform_domain').'/mini/wallet/topup-complete',
]);
$this->rethrowValidation($response, 'amount');
$checkoutUrl = (string) $response->json('data.checkout_url', '');
if ($checkoutUrl === '') {
return response()->json(['message' => 'Could not start top-up. Please try again.'], 422);
}
return response()->json(['data' => ['checkout_url' => $checkoutUrl]]);
}
public function banks(Request $request): JsonResponse
{
$type = $request->query('type') === 'mobile_money' ? 'mobile_money' : 'bank';
$response = $this->identitySend('GET', '/api/identity/banks?type='.urlencode($type), []);
return response()->json(['data' => $response->json('data', [])]);
}
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');
$this->notifications->withdrawalSubmitted(
ladill_account(),
(float) $data['amount'],
);
return response()->json(['data' => $response->json('data', [])]);
}
}