feat: enhance payment status handling and summary in MyPaymentsPage

- Added support for 'partial' payment status in MyPaymentsPage and related components.
- Updated API responses to include 'paid_rials' and 'summary' for invoices.
- Introduced InvoicePaymentStatus service to derive payment status based on actual payments.
- Enhanced tests to cover new payment scenarios including partial payments and payment methods.
- Updated documentation to reflect changes in payment status and API responses.
This commit is contained in:
hamed
2026-07-19 10:38:29 +03:30
parent 5c8fe8ece4
commit 357adb1d14
18 changed files with 620 additions and 188 deletions
+59 -8
View File
@@ -6,6 +6,8 @@ use App\Auth\Entity\User;
use App\Billing\Entity\Invoice;
use App\Doctor\Entity\Doctor;
use App\Patient\Entity\PatientRecord;
use App\Patient\Entity\PatientSession;
use App\Patient\Entity\SessionPayment;
use App\Tests\ApiTestCase;
/**
@@ -45,10 +47,38 @@ class PaymentsSummaryTest extends ApiTestCase
$ref->setValue($obj, $value);
}
private function invoice(Doctor $doctor, PatientRecord $record, int $patientRials, string $status, int $issuedAt): Invoice
/** مراجعه‌ای با یک پرداخت نقدی ثبت‌شده — منبع واقعیِ «چقدر وصول شده». */
private function paidSession(PatientRecord $record, int $paidRials): PatientSession
{
$session = new PatientSession($record);
$this->em->persist($session);
$this->em->flush();
$payment = new SessionPayment($session, 'cash', $paidRials, 1_700_000_000);
$this->em->persist($payment);
$this->em->flush();
return $session;
}
/**
* `totalRials` دو برابر سهم بیمار است تا دو خلاصه از هم قابل‌تفکیک بمانند.
* `$paidRials` پرداخت واقعی روی مراجعه است؛ وضعیت پرداخت از همین مشتق می‌شود.
*/
private function invoice(
Doctor $doctor,
PatientRecord $record,
int $patientRials,
string $status,
int $issuedAt,
int $paidRials = 0,
): Invoice {
$invoice = new Invoice('doctor', $doctor->getId());
$invoice->setPatientRecordId($record->getId());
if ($paidRials > 0) {
$invoice->setPatientSessionId($this->paidSession($record, $paidRials)->getId());
}
$this->setField($invoice, 'totalRials', $patientRials * 2);
$this->setField($invoice, 'patientRials', $patientRials);
$this->setField($invoice, 'status', $status);
$this->setField($invoice, 'issuedAt', $issuedAt);
@@ -63,10 +93,10 @@ class PaymentsSummaryTest extends ApiTestCase
[$owner, $doctor] = $this->doctor();
$record = $this->patientRecord($doctor);
$this->invoice($doctor, $record, 1_000_000, Invoice::STATUS_PAID, 1_700_000_000);
$this->invoice($doctor, $record, 1_000_000, Invoice::STATUS_FINALIZED, 1_700_000_000, 1_000_000);
$this->invoice($doctor, $record, 400_000, Invoice::STATUS_FINALIZED, 1_700_000_100);
// draft invoices are not part of the payments list, so they must not count
$this->invoice($doctor, $record, 999_000, Invoice::STATUS_DRAFT, 1_700_000_200);
$this->invoice($doctor, $record, 999_000, Invoice::STATUS_DRAFT, 1_700_000_200, 999_000);
$res = $this->authJson('GET', '/api/v1/my/billing/payments/summary', $owner);
self::assertSame(200, $this->responseCode());
@@ -83,9 +113,9 @@ class PaymentsSummaryTest extends ApiTestCase
[$owner, $doctor] = $this->doctor();
$record = $this->patientRecord($doctor);
$this->invoice($doctor, $record, 1_000_000, Invoice::STATUS_PAID, 1_700_000_000);
$this->invoice($doctor, $record, 1_000_000, Invoice::STATUS_FINALIZED, 1_700_000_000, 1_000_000);
$this->invoice($doctor, $record, 400_000, Invoice::STATUS_FINALIZED, 1_700_000_000);
$this->invoice($doctor, $record, 700_000, Invoice::STATUS_PAID, 1_800_000_000);
$this->invoice($doctor, $record, 700_000, Invoice::STATUS_FINALIZED, 1_800_000_000, 700_000);
$res = $this->authJson('GET', '/api/v1/my/billing/payments/summary?status=paid', $owner);
self::assertSame(1_700_000, $res['data']['total_rials']);
@@ -106,8 +136,8 @@ class PaymentsSummaryTest extends ApiTestCase
$mine = $this->patientRecord($doctor, $code);
$other = $this->patientRecord($doctor, (string) random_int(1_000_000_000, 9_999_999_999));
$this->invoice($doctor, $mine, 500_000, Invoice::STATUS_PAID, 1_700_000_000);
$this->invoice($doctor, $other, 800_000, Invoice::STATUS_PAID, 1_700_000_000);
$this->invoice($doctor, $mine, 500_000, Invoice::STATUS_FINALIZED, 1_700_000_000, 500_000);
$this->invoice($doctor, $other, 800_000, Invoice::STATUS_FINALIZED, 1_700_000_000, 800_000);
$res = $this->authJson('GET', '/api/v1/my/billing/payments/summary?national_code=' . $code, $owner);
self::assertSame(500_000, $res['data']['total_rials']);
@@ -126,11 +156,32 @@ class PaymentsSummaryTest extends ApiTestCase
);
}
public function testPatientInvoicesCarryTheirOwnSummary(): void
{
[$owner, $doctor] = $this->doctor();
$record = $this->patientRecord($doctor);
$this->invoice($doctor, $record, 1_000_000, Invoice::STATUS_FINALIZED, 1_700_000_000, 1_000_000);
$this->invoice($doctor, $record, 500_000, Invoice::STATUS_FINALIZED, 1_700_000_100);
$this->invoice($doctor, $record, 900_000, Invoice::STATUS_DRAFT, 1_700_000_200, 900_000);
$res = $this->authJson('GET', '/api/v1/my/billing/patients/' . $record->getUuid() . '/invoices', $owner);
self::assertSame(200, $this->responseCode());
// the patient view sums total_rials (invoice grand total), not the patient share
$summary = $res['data']['summary'];
self::assertSame(3_000_000, $summary['total_rials']);
self::assertSame(2_000_000, $summary['paid_rials']);
self::assertSame(1_000_000, $summary['unsettled_rials']);
self::assertSame(2, $summary['invoices_count']);
self::assertSame(2, $res['data']['meta']['totalRecords']);
}
public function testSummaryExcludesOtherTenants(): void
{
[$owner, $doctor] = $this->doctor();
$record = $this->patientRecord($doctor);
$this->invoice($doctor, $record, 300_000, Invoice::STATUS_PAID, 1_700_000_000);
$this->invoice($doctor, $record, 300_000, Invoice::STATUS_FINALIZED, 1_700_000_000, 300_000);
[$otherOwner] = $this->doctor();
$res = $this->authJson('GET', '/api/v1/my/billing/payments/summary', $otherOwner);