Files
clinicpro/tests/Billing/PaymentsSummaryTest.php
T
hamed 5c8fe8ece4 feat: add payments summary endpoint and UI redesign for MyPaymentsPage
- Implemented a new API endpoint `/api/v1/my/billing/payments/summary` to provide a financial summary of payments with filters for national code, status, and date range.
- Updated the InvoiceRepository to aggregate totals for paid and unsettled invoices.
- Created a new hook `usePaymentsSummary` to fetch summary data in the frontend.
- Redesigned the MyPaymentsPage to align with the ClaimsPage structure, incorporating a design system, summary statistics, and improved filtering options.
- Added tests for the new payments summary endpoint to ensure correct functionality and filtering behavior.
2026-07-19 10:12:01 +03:30

141 lines
5.6 KiB
PHP

<?php
namespace App\Tests\Billing;
use App\Auth\Entity\User;
use App\Billing\Entity\Invoice;
use App\Doctor\Entity\Doctor;
use App\Patient\Entity\PatientRecord;
use App\Tests\ApiTestCase;
/**
* GET /api/v1/my/billing/payments/summary — aggregate totals over the same
* filtered set as the payments list, feeding the admin page's stat cards.
*/
class PaymentsSummaryTest extends ApiTestCase
{
/** @return array{0: User, 1: Doctor} owner user + their doctor profile */
private function doctor(): array
{
$owner = $this->createUser(['ROLE_DOCTOR']);
$doctor = new Doctor($owner, 'دکتر تست');
$this->em->persist($doctor);
$this->em->flush();
return [$owner, $doctor];
}
private function patientRecord(Doctor $doctor, ?string $nationalCode = null): PatientRecord
{
$patient = $this->createUser(['ROLE_USER']);
if ($nationalCode !== null) {
$patient->setNationalCode($nationalCode);
}
$record = new PatientRecord('doctor', $doctor->getId(), $patient, 'doctor', $doctor->getId());
$this->em->persist($record);
$this->em->flush();
return $record;
}
private function setField(object $obj, string $prop, mixed $value): void
{
$ref = new \ReflectionProperty($obj, $prop);
$ref->setAccessible(true);
$ref->setValue($obj, $value);
}
private function invoice(Doctor $doctor, PatientRecord $record, int $patientRials, string $status, int $issuedAt): Invoice
{
$invoice = new Invoice('doctor', $doctor->getId());
$invoice->setPatientRecordId($record->getId());
$this->setField($invoice, 'patientRials', $patientRials);
$this->setField($invoice, 'status', $status);
$this->setField($invoice, 'issuedAt', $issuedAt);
$this->em->persist($invoice);
$this->em->flush();
return $invoice;
}
public function testSummarySplitsPaidAndUnsettled(): void
{
[$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, 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);
$res = $this->authJson('GET', '/api/v1/my/billing/payments/summary', $owner);
self::assertSame(200, $this->responseCode());
$data = $res['data'];
self::assertSame(1_400_000, $data['total_rials']);
self::assertSame(1_000_000, $data['paid_rials']);
self::assertSame(400_000, $data['unsettled_rials']);
self::assertSame(2, $data['invoices_count']);
}
public function testSummaryHonoursStatusAndDateFilters(): void
{
[$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, 400_000, Invoice::STATUS_FINALIZED, 1_700_000_000);
$this->invoice($doctor, $record, 700_000, Invoice::STATUS_PAID, 1_800_000_000);
$res = $this->authJson('GET', '/api/v1/my/billing/payments/summary?status=paid', $owner);
self::assertSame(1_700_000, $res['data']['total_rials']);
self::assertSame(1_700_000, $res['data']['paid_rials']);
self::assertSame(0, $res['data']['unsettled_rials']);
self::assertSame(2, $res['data']['invoices_count']);
$res = $this->authJson('GET', '/api/v1/my/billing/payments/summary?from=1700000000&to=1700000001', $owner);
self::assertSame(1_400_000, $res['data']['total_rials']);
self::assertSame(2, $res['data']['invoices_count']);
}
public function testSummaryFiltersByNationalCode(): void
{
[$owner, $doctor] = $this->doctor();
// db_test is never reset — a fixed code would eventually collide on the unique column
$code = (string) random_int(1_000_000_000, 9_999_999_999);
$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);
$res = $this->authJson('GET', '/api/v1/my/billing/payments/summary?national_code=' . $code, $owner);
self::assertSame(500_000, $res['data']['total_rials']);
self::assertSame(1, $res['data']['invoices_count']);
}
public function testSummaryIsZeroWhenNothingMatches(): void
{
[$owner] = $this->doctor();
$res = $this->authJson('GET', '/api/v1/my/billing/payments/summary', $owner);
self::assertSame(200, $this->responseCode());
self::assertSame(
['total_rials' => 0, 'paid_rials' => 0, 'unsettled_rials' => 0, 'invoices_count' => 0],
$res['data'],
);
}
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);
[$otherOwner] = $this->doctor();
$res = $this->authJson('GET', '/api/v1/my/billing/payments/summary', $otherOwner);
self::assertSame(0, $res['data']['total_rials']);
self::assertSame(0, $res['data']['invoices_count']);
}
}