feat: port wallet charge/withdraw modal from tauri to patient admin page

Add manual wallet withdrawal (debit) endpoint mirroring the offline app's
balance guard, and rebuild the patient کیف پول tab around a single
charge/withdraw toggle modal (quick amounts, تومان→ریال conversion,
transaction filters).

Backend:
- POST /api/v1/patient/{uuid}/wallet/withdraw — creates a debit
  WalletTransaction; 422 ERR_WALLET_INSUFFICIENT when amount exceeds balance.
- ErrorCodes: ERR_WALLET_INSUFFICIENT ('موجودی کیف پول کافی نیست').
- docs/api/patient.md updated.

Frontend:
- usePatientWallet hook (balance + charge/withdraw mutations).
- WalletTransactionModal (toggle, quick amounts, UI-only payment fields).
- WalletTab: charge button, همه/واریزی/برداشت filters.

Tests: backend withdraw success/insufficient/non-positive/ownership;
frontend modal + wallet tab interactions.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
hamed
2026-07-16 11:56:39 +03:30
co-authored by Claude Opus 4.8
parent 4a70c87a8b
commit f028d6841a
9 changed files with 533 additions and 46 deletions
@@ -168,6 +168,46 @@ class PatientController extends BaseController
], 201);
}
/**
* Manual wallet withdrawal (برداشت از کیف پول) — e.g. a refund or cash
* hand-back at the desk. Creates a debit WalletTransaction for the record's
* owner User. Rejected (422) when the amount exceeds the current balance,
* mirroring the offline app's balance guard.
*/
#[Route('/api/v1/patient/{uuid}/wallet/withdraw', methods: ['POST'])]
public function withdrawWallet(string $uuid, Request $request, #[CurrentUser] User $user): JsonResponse
{
[$entityType, $entityId] = $this->resolveEntity($user);
$record = $this->recordRepo->findByUuid($uuid);
if ($record === null || !$this->ownsRecord($record, $entityType, $entityId)) {
return $this->error(ErrorCodes::ERR_PATIENT_NOT_FOUND, ErrorCodes::message(ErrorCodes::ERR_PATIENT_NOT_FOUND), 404);
}
$data = json_decode($request->getContent(), true) ?? [];
$amount = (int) ($data['amount_rials'] ?? 0);
if ($amount <= 0) {
return $this->error(ErrorCodes::ERR_VALIDATION_001, 'مبلغ برداشت باید بزرگ‌تر از صفر باشد', 422, 'amount_rials');
}
$patient = $record->getUser();
$current = $this->settlementRepo->getWalletBalance($patient);
if ($amount > $current) {
return $this->error(ErrorCodes::ERR_WALLET_INSUFFICIENT, ErrorCodes::message(ErrorCodes::ERR_WALLET_INSUFFICIENT), 422, 'amount_rials');
}
$balance = $current - $amount;
$txn = new \App\Settlement\Entity\WalletTransaction($patient, $amount, 'debit', $balance);
$description = trim((string) ($data['description'] ?? ''));
$txn->setDescription($description !== '' ? $description : 'برداشت از کیف پول');
$this->walletRepo->save($txn);
return $this->success([
'transaction' => $txn->toArray(),
'balance_rials' => $balance,
], 201);
}
// ── Call center (کال سنتر) ─────────────────────────────────────────────────
/** List the patient's call log (newest first, optional ?outcome=success|missed). */