feat(patients): phase C — patient financial tabs (payments, wallet, transactions)
The generic wallet/payment endpoints are bound to #[CurrentUser] (the
requester's own money), so they cannot serve a record owner viewing a patient's
finances. Add three record-owner-gated read endpoints on PatientController that
reuse the existing repositories:
GET /patient/{uuid}/payments — paginated gateway payments (?status)
GET /patient/{uuid}/wallet — balance + 10 recent transactions
GET /patient/{uuid}/wallet/transactions — full paginated ledger
Wire the previously-placeholder "پرداختها" and "کیف پول" tabs on the patient
detail page: payments via the shared TabList, wallet via a new WalletTab (balance
card + credit/debit ledger). PatientFinancialsTest covers the happy path, the
credit−debit balance, and ownership scoping (404 for a different owner).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -54,9 +54,85 @@ class PatientController extends BaseController
|
||||
private readonly \App\Patient\Repository\PatientMedicalRecordRepository $medicalRepo,
|
||||
private readonly \App\Patient\Repository\PatientMessageRepository $messageRepo,
|
||||
private readonly \App\Shared\Service\FileUploadService $fileUpload,
|
||||
private readonly \App\Payment\Repository\PaymentRepository $paymentRepo,
|
||||
private readonly \App\Settlement\Repository\WalletTransactionRepository $walletRepo,
|
||||
private readonly \App\Settlement\Repository\SettlementRepository $settlementRepo,
|
||||
private readonly LoggerInterface $logger,
|
||||
) {}
|
||||
|
||||
// ── Financials (مالی: پرداخت / تراکنش / کیفپول) ────────────────────────────
|
||||
//
|
||||
// The patient's money is scoped to the record's owner User. The generic
|
||||
// wallet/payment endpoints are bound to #[CurrentUser] (the requester's own
|
||||
// finances), so a doctor/secretary viewing a record needs these
|
||||
// record-owner-gated reads to see the *patient's* finances instead.
|
||||
|
||||
/** List gateway payments made by the patient (paginated, optional ?status). */
|
||||
#[Route('/api/v1/patient/{uuid}/payments', methods: ['GET'])]
|
||||
public function listPayments(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);
|
||||
}
|
||||
|
||||
$page = max(1, (int) $request->query->get('page', 1));
|
||||
$limit = min(100, max(1, (int) $request->query->get('limit', 20)));
|
||||
$status = $request->query->get('status') ?: null;
|
||||
$patient = $record->getUser();
|
||||
|
||||
$payments = array_map(
|
||||
fn(\App\Payment\Entity\Payment $p) => $p->toArray(),
|
||||
$this->paymentRepo->findByUser($patient, $status, $page, $limit)
|
||||
);
|
||||
|
||||
return $this->paginated($payments, $this->paymentRepo->countByUser($patient, $status), $page, $limit);
|
||||
}
|
||||
|
||||
/** Patient wallet balance + the 10 most recent transactions (کیفپول tab). */
|
||||
#[Route('/api/v1/patient/{uuid}/wallet', methods: ['GET'])]
|
||||
public function walletBalance(string $uuid, #[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);
|
||||
}
|
||||
|
||||
$patient = $record->getUser();
|
||||
|
||||
return $this->success([
|
||||
'balance_rials' => $this->settlementRepo->getWalletBalance($patient),
|
||||
'recent_transactions' => array_map(
|
||||
fn(\App\Settlement\Entity\WalletTransaction $t) => $t->toArray(),
|
||||
$this->walletRepo->findByUser($patient, 10)
|
||||
),
|
||||
]);
|
||||
}
|
||||
|
||||
/** Full paginated wallet-transaction ledger for the patient (تراکنش tab). */
|
||||
#[Route('/api/v1/patient/{uuid}/wallet/transactions', methods: ['GET'])]
|
||||
public function walletTransactions(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);
|
||||
}
|
||||
|
||||
$page = max(1, (int) $request->query->get('page', 1));
|
||||
$limit = min(100, max(1, (int) $request->query->get('limit', 50)));
|
||||
$patient = $record->getUser();
|
||||
|
||||
$txns = array_map(
|
||||
fn(\App\Settlement\Entity\WalletTransaction $t) => $t->toArray(),
|
||||
$this->walletRepo->findByUser($patient, $limit, ($page - 1) * $limit)
|
||||
);
|
||||
|
||||
return $this->paginated($txns, $this->walletRepo->countByUser($patient), $page, $limit);
|
||||
}
|
||||
|
||||
// ── Messages (پیامها) ─────────────────────────────────────────────────────
|
||||
|
||||
#[Route('/api/v1/patient/{uuid}/messages', methods: ['GET'])]
|
||||
|
||||
Reference in New Issue
Block a user