feat: unify wallet with real payment methods, full transaction transparency, pay-session-from-wallet
Address wallet feedback: use the clinic's real payment infrastructure,
redesign the tab to match the admin panel, and make every wallet movement
fully auditable.
Backend:
- WalletTransaction: add createdBy (acting user) + createdByName, payment_method,
reference, status; toArray exposes them (migration Version20260716083939).
- WalletService (Settlement): balance/charge/withdraw + settleSessionFromWallet,
records actor/method/reason; insufficient balance throws ERR_WALLET_INSUFFICIENT.
- PatientController: charge/withdraw delegate to WalletService and accept
payment_method/reference; PATCH /session/{uuid} with payment_method=wallet
debits the patient's final share from the wallet (reference=session:{uuid}).
- docs/api/patient.md updated.
Frontend:
- Wallet modal redesigned to panel style (no gradient); payment method now uses
the clinic's real bank accounts + POS devices (usePaymentMethods) plus cash.
- Wallet tab: panel balance card + DataTable ledger with columns مبلغ/نوع/روش/
دلیل/ثبتکننده/تاریخ/ساعت/وضعیت + همه/واریزی/برداشت filters.
- Session card «تکمیل پرداخت» opens a payment-method chooser incl. کیف پول.
Tests: backend transparency + session-from-wallet (success/insufficient/cash);
frontend modal (real methods, toman→rials) + wallet tab + settle chooser.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -58,6 +58,7 @@ class PatientController extends BaseController
|
||||
private readonly \App\Payment\Repository\PaymentRepository $paymentRepo,
|
||||
private readonly \App\Settlement\Repository\WalletTransactionRepository $walletRepo,
|
||||
private readonly \App\Settlement\Repository\SettlementRepository $settlementRepo,
|
||||
private readonly \App\Settlement\Service\WalletService $walletService,
|
||||
private readonly LoggerInterface $logger,
|
||||
) {}
|
||||
|
||||
@@ -155,24 +156,24 @@ class PatientController extends BaseController
|
||||
}
|
||||
|
||||
$patient = $record->getUser();
|
||||
$balance = $this->settlementRepo->getWalletBalance($patient) + $amount;
|
||||
|
||||
$txn = new \App\Settlement\Entity\WalletTransaction($patient, $amount, 'credit', $balance);
|
||||
$description = trim((string) ($data['description'] ?? ''));
|
||||
$txn->setDescription($description !== '' ? $description : 'شارژ کیف پول');
|
||||
$this->walletRepo->save($txn);
|
||||
$txn = $this->walletService->charge(
|
||||
$patient, $amount, $user,
|
||||
trim((string) ($data['description'] ?? '')) ?: null,
|
||||
$this->normalizeMethod($data['payment_method'] ?? null),
|
||||
trim((string) ($data['reference'] ?? '')) ?: null,
|
||||
);
|
||||
|
||||
return $this->success([
|
||||
'transaction' => $txn->toArray(),
|
||||
'balance_rials' => $balance,
|
||||
'balance_rials' => $txn->getBalanceAfter(),
|
||||
], 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.
|
||||
* owner User (with acting user + payment method recorded). Rejected (422)
|
||||
* when the amount exceeds the current balance, mirroring the offline app.
|
||||
*/
|
||||
#[Route('/api/v1/patient/{uuid}/wallet/withdraw', methods: ['POST'])]
|
||||
public function withdrawWallet(string $uuid, Request $request, #[CurrentUser] User $user): JsonResponse
|
||||
@@ -189,25 +190,28 @@ class PatientController extends BaseController
|
||||
return $this->error(ErrorCodes::ERR_VALIDATION_001, 'مبلغ برداشت باید بزرگتر از صفر باشد', 422, 'amount_rials');
|
||||
}
|
||||
|
||||
// Insufficient balance → WalletService throws AppException (422), handled globally.
|
||||
$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);
|
||||
$txn = $this->walletService->withdraw(
|
||||
$patient, $amount, $user,
|
||||
trim((string) ($data['description'] ?? '')) ?: null,
|
||||
$this->normalizeMethod($data['payment_method'] ?? null),
|
||||
trim((string) ($data['reference'] ?? '')) ?: null,
|
||||
);
|
||||
|
||||
return $this->success([
|
||||
'transaction' => $txn->toArray(),
|
||||
'balance_rials' => $balance,
|
||||
'balance_rials' => $txn->getBalanceAfter(),
|
||||
], 201);
|
||||
}
|
||||
|
||||
/** روش پرداختِ مجاز برای کیف پول؛ ورودیِ ناشناخته نادیده گرفته میشود. */
|
||||
private function normalizeMethod(mixed $method): ?string
|
||||
{
|
||||
$method = is_string($method) ? trim($method) : '';
|
||||
return in_array($method, ['card', 'pos', 'cash', 'gateway', 'wallet'], true) ? $method : null;
|
||||
}
|
||||
|
||||
// ── Call center (کال سنتر) ─────────────────────────────────────────────────
|
||||
|
||||
/** List the patient's call log (newest first, optional ?outcome=success|missed). */
|
||||
@@ -958,8 +962,20 @@ class PatientController extends BaseController
|
||||
|
||||
$data = json_decode($request->getContent(), true) ?? [];
|
||||
|
||||
if (isset($data['notes'])) { $session->setNotes($data['notes']); }
|
||||
if (isset($data['payment_method'])) { $session->setPaymentMethod($data['payment_method']); }
|
||||
if (isset($data['notes'])) { $session->setNotes($data['notes']); }
|
||||
|
||||
if (isset($data['payment_method'])) {
|
||||
$method = (string) $data['payment_method'];
|
||||
// پرداخت از کیف پول: سهمِ بیمار را از موجودی کسر کن (فقط یکبار، اگر
|
||||
// مراجعه هنوز تسویه نشده). موجودیِ ناکافی → AppException (۴۲۲).
|
||||
if ($method === 'wallet'
|
||||
&& $session->getPaymentMethod() === 'pending'
|
||||
&& $session->getFinalPriceRials() > 0
|
||||
) {
|
||||
$this->walletService->settleSessionFromWallet($session, $user);
|
||||
}
|
||||
$session->setPaymentMethod($method);
|
||||
}
|
||||
|
||||
$this->sessionRepo->save($session);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user