1. شارژ کیف پول is now functional end-to-end. New owner-gated
POST /api/v1/patient/{uuid}/wallet/charge creates a manual credit
WalletTransaction (computed balance_after); the patient detail's wallet tab
gains a top-up modal (PriceInput + description) and supports ?tab= deep
links. The deposit sections of the create drawer, the edit page and the
replace modal link to it via WalletChargeLink (record resolved by mobile).
2. جایگزینی نوبت now matches appointments-replace.pdf: patient search-or-new,
بخش/سرویس/پرسنل selects prefilled from the appointment, deposit toggle +
amount + charge link, read-only original date/time, status pick and notes —
all through the general PATCH.
3. The confirmed-appointments table is paginated (20/page, client-side so the
schedule view and doctor-tab derivation keep the whole day), resetting on
date/doctor/filter changes. The page-local STATUS_META also adopts the
design labels plus following_up/salon for the schedule cards.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
119 lines
4.8 KiB
PHP
119 lines
4.8 KiB
PHP
<?php
|
|
|
|
namespace App\Tests\Patient;
|
|
|
|
use App\Doctor\Entity\Doctor;
|
|
use App\Patient\Entity\PatientRecord;
|
|
use App\Payment\Entity\Payment;
|
|
use App\Settlement\Entity\WalletTransaction;
|
|
use App\Tests\ApiTestCase;
|
|
|
|
/**
|
|
* Record-owner-gated reads of the patient's finances:
|
|
* payments list, wallet balance, and wallet-transaction ledger.
|
|
*/
|
|
class PatientFinancialsTest extends ApiTestCase
|
|
{
|
|
/** @return array{0: \App\Auth\Entity\User, 1: PatientRecord, 2: \App\Auth\Entity\User} */
|
|
private function recordFor(): array
|
|
{
|
|
$owner = $this->createUser(['ROLE_DOCTOR']);
|
|
$doctor = new Doctor($owner, 'دکتر');
|
|
$this->em->persist($doctor);
|
|
$this->em->flush();
|
|
|
|
$patient = $this->createUser(['ROLE_USER']);
|
|
$record = new PatientRecord('doctor', $doctor->getId(), $patient, 'doctor', $doctor->getId());
|
|
$this->em->persist($record);
|
|
$this->em->flush();
|
|
|
|
return [$owner, $record, $patient];
|
|
}
|
|
|
|
public function testListsPatientPayments(): void
|
|
{
|
|
[$owner, $record, $patient] = $this->recordFor();
|
|
|
|
$payment = new Payment($patient, 250000, 'mellat', 'appointment');
|
|
$payment->setStatus(Payment::STATUS_SUCCESS);
|
|
$this->em->persist($payment);
|
|
$this->em->flush();
|
|
|
|
$res = $this->authJson('GET', '/api/v1/patient/' . $record->getUuid() . '/payments', $owner);
|
|
self::assertSame(200, $this->responseCode());
|
|
self::assertCount(1, $res['data']);
|
|
self::assertSame(250000, $res['data'][0]['amount_rials']);
|
|
self::assertSame(1, $res['meta']['totalRecords']);
|
|
}
|
|
|
|
public function testWalletBalanceReflectsCreditMinusDebit(): void
|
|
{
|
|
[$owner, $record, $patient] = $this->recordFor();
|
|
|
|
$this->em->persist(new WalletTransaction($patient, 500000, 'credit', 500000));
|
|
$this->em->persist(new WalletTransaction($patient, 200000, 'debit', 300000));
|
|
$this->em->flush();
|
|
|
|
$res = $this->authJson('GET', '/api/v1/patient/' . $record->getUuid() . '/wallet', $owner);
|
|
self::assertSame(200, $this->responseCode());
|
|
self::assertSame(300000, $res['data']['balance_rials']);
|
|
self::assertCount(2, $res['data']['recent_transactions']);
|
|
}
|
|
|
|
public function testListsWalletTransactionsPaginated(): void
|
|
{
|
|
[$owner, $record, $patient] = $this->recordFor();
|
|
|
|
$this->em->persist(new WalletTransaction($patient, 100000, 'credit', 100000));
|
|
$this->em->flush();
|
|
|
|
$res = $this->authJson('GET', '/api/v1/patient/' . $record->getUuid() . '/wallet/transactions', $owner);
|
|
self::assertSame(200, $this->responseCode());
|
|
self::assertCount(1, $res['data']);
|
|
self::assertSame('credit', $res['data'][0]['type']);
|
|
self::assertSame(1, $res['meta']['totalRecords']);
|
|
}
|
|
|
|
public function testChargeWalletCreatesCreditAndReturnsBalance(): void
|
|
{
|
|
[$owner, $record, $patient] = $this->recordFor();
|
|
|
|
$this->em->persist(new WalletTransaction($patient, 200000, 'credit', 200000));
|
|
$this->em->flush();
|
|
|
|
$res = $this->authJson('POST', '/api/v1/patient/' . $record->getUuid() . '/wallet/charge', $owner, [
|
|
'amount_rials' => 300000, 'description' => 'بیعانه نوبت',
|
|
]);
|
|
self::assertSame(201, $this->responseCode());
|
|
self::assertSame(500000, $res['data']['balance_rials']);
|
|
self::assertSame('credit', $res['data']['transaction']['type']);
|
|
self::assertSame('بیعانه نوبت', $res['data']['transaction']['description']);
|
|
|
|
$wallet = $this->authJson('GET', '/api/v1/patient/' . $record->getUuid() . '/wallet', $owner);
|
|
self::assertSame(500000, $wallet['data']['balance_rials']);
|
|
}
|
|
|
|
public function testChargeWalletRejectsNonPositiveAmount(): void
|
|
{
|
|
[$owner, $record] = $this->recordFor();
|
|
$this->authJson('POST', '/api/v1/patient/' . $record->getUuid() . '/wallet/charge', $owner, ['amount_rials' => 0]);
|
|
self::assertSame(422, $this->responseCode());
|
|
}
|
|
|
|
public function testFinancialsAreOwnershipScoped(): void
|
|
{
|
|
[, $record] = $this->recordFor();
|
|
[$other] = $this->recordFor();
|
|
|
|
// A different owner cannot read this record's finances (or charge them).
|
|
$this->authJson('GET', '/api/v1/patient/' . $record->getUuid() . '/payments', $other);
|
|
self::assertSame(404, $this->responseCode());
|
|
$this->authJson('POST', '/api/v1/patient/' . $record->getUuid() . '/wallet/charge', $other, ['amount_rials' => 1000]);
|
|
self::assertSame(404, $this->responseCode());
|
|
$this->authJson('GET', '/api/v1/patient/' . $record->getUuid() . '/wallet', $other);
|
|
self::assertSame(404, $this->responseCode());
|
|
$this->authJson('GET', '/api/v1/patient/' . $record->getUuid() . '/wallet/transactions', $other);
|
|
self::assertSame(404, $this->responseCode());
|
|
}
|
|
}
|