Port the remaining gap of tauri /files/invoice-summary into the admin
invoice summary: GET /api/v1/billing/invoices/{uuid} now includes a
'session' key (full PatientSession payload) so InvoiceSummaryModal can
render the consumables table, the itemized payments table (method,
amount, date-time, recorder) and real discount/paid/remaining figures
instead of heuristics. Invoices without a source session keep the
previous behavior (session: null).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
172 lines
6.2 KiB
PHP
172 lines
6.2 KiB
PHP
<?php
|
||
|
||
namespace App\Tests\Billing;
|
||
|
||
use App\Auth\Entity\User;
|
||
use App\Billing\Entity\Invoice;
|
||
use App\Doctor\Entity\Doctor;
|
||
use App\Inventory\Entity\InventoryItem;
|
||
use App\Patient\Entity\PatientRecord;
|
||
use App\Patient\Entity\PatientSession;
|
||
use App\Patient\Entity\SessionConsumable;
|
||
use App\Patient\Entity\SessionPayment;
|
||
use App\Tests\ApiTestCase;
|
||
|
||
/**
|
||
* GET /api/v1/billing/invoices/{uuid} — invoice detail enriched with its
|
||
* source session (`session` key: payments, consumables, discount, paid totals).
|
||
*/
|
||
class InvoiceShowSessionTest 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): PatientRecord
|
||
{
|
||
$patient = $this->createUser(['ROLE_USER']);
|
||
$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);
|
||
}
|
||
|
||
public function testShowIncludesSessionPaymentsConsumablesAndDiscount(): void
|
||
{
|
||
[$owner, $doctor] = $this->doctor();
|
||
$record = $this->patientRecord($doctor);
|
||
|
||
$session = new PatientSession($record);
|
||
$session->setServicesTotalRials(2_400_000)
|
||
->setFinalPriceRials(2_200_000)
|
||
->setDiscount('amount', 200_000, 200_000)
|
||
->setSessionAt(1_700_000_000)
|
||
->setPaidAt(1_700_100_000);
|
||
$this->em->persist($session);
|
||
$this->em->flush();
|
||
|
||
$payment = new SessionPayment($session, 'wallet', 1_500_000, 1_700_100_000);
|
||
$payment->setCreatedByName('منشی تست');
|
||
$this->em->persist($payment);
|
||
$session->addPayment($payment);
|
||
|
||
$item = new InventoryItem('doctor', $doctor->getId(), 'عینک');
|
||
$item->setPrice(20_000);
|
||
$this->em->persist($item);
|
||
$this->em->flush();
|
||
|
||
$consumable = new SessionConsumable($session, $item, 2);
|
||
$this->em->persist($consumable);
|
||
$session->addConsumable($consumable);
|
||
$this->em->flush();
|
||
|
||
$invoice = new Invoice('doctor', $doctor->getId());
|
||
$invoice->setPatientSessionId($session->getId())
|
||
->setPatientRecordId($record->getId());
|
||
$this->em->persist($invoice);
|
||
$this->em->flush();
|
||
|
||
$res = $this->authJson('GET', '/api/v1/billing/invoices/' . $invoice->getUuid(), $owner);
|
||
self::assertSame(200, $this->responseCode());
|
||
|
||
$data = $res['data']['data'];
|
||
self::assertSame($invoice->getUuid(), $data['uuid']);
|
||
self::assertIsArray($data['session']);
|
||
|
||
$s = $data['session'];
|
||
self::assertSame($session->getUuid(), $s['uuid']);
|
||
self::assertSame(1_700_000_000, $s['session_at']);
|
||
self::assertSame(1_700_100_000, $s['paid_at']);
|
||
self::assertSame(2_400_000, $s['services_total_rials']);
|
||
self::assertSame(2_200_000, $s['final_price_rials']);
|
||
self::assertSame(200_000, $s['discount_rials']);
|
||
|
||
// payments — real rows, not heuristics
|
||
self::assertCount(1, $s['payments']);
|
||
self::assertSame('wallet', $s['payments'][0]['method']);
|
||
self::assertSame(1_500_000, $s['payments'][0]['amount_rials']);
|
||
self::assertSame('منشی تست', $s['payments'][0]['created_by_name']);
|
||
self::assertSame(1_500_000, $s['paid_total_rials']);
|
||
|
||
// consumables — snapshot price × quantity
|
||
self::assertCount(1, $s['consumables']);
|
||
self::assertSame('عینک', $s['consumables'][0]['item_name']);
|
||
self::assertSame(2, $s['consumables'][0]['quantity']);
|
||
self::assertSame(40_000, $s['consumables'][0]['line_total_rials']);
|
||
self::assertSame(40_000, $s['consumables_total_rials']);
|
||
}
|
||
|
||
public function testShowSessionNullWhenInvoiceHasNoSession(): void
|
||
{
|
||
[$owner, $doctor] = $this->doctor();
|
||
$record = $this->patientRecord($doctor);
|
||
|
||
$invoice = new Invoice('doctor', $doctor->getId());
|
||
$invoice->setPatientRecordId($record->getId());
|
||
$this->em->persist($invoice);
|
||
$this->em->flush();
|
||
|
||
$res = $this->authJson('GET', '/api/v1/billing/invoices/' . $invoice->getUuid(), $owner);
|
||
self::assertSame(200, $this->responseCode());
|
||
self::assertNull($res['data']['data']['session']);
|
||
}
|
||
|
||
public function testShowNotFoundForUnknownUuid(): void
|
||
{
|
||
[$owner] = $this->doctor();
|
||
$this->authJson('GET', '/api/v1/billing/invoices/00000000-0000-0000-0000-000000000000', $owner);
|
||
self::assertSame(404, $this->responseCode());
|
||
}
|
||
|
||
public function testShowNotFoundForOtherTenant(): void
|
||
{
|
||
[, $doctor] = $this->doctor();
|
||
$invoice = new Invoice('doctor', $doctor->getId());
|
||
$this->em->persist($invoice);
|
||
$this->em->flush();
|
||
|
||
[$other] = $this->doctor();
|
||
$this->authJson('GET', '/api/v1/billing/invoices/' . $invoice->getUuid(), $other);
|
||
self::assertSame(404, $this->responseCode());
|
||
}
|
||
|
||
public function testShowSessionEmptyPaymentsAndConsumables(): void
|
||
{
|
||
[$owner, $doctor] = $this->doctor();
|
||
$record = $this->patientRecord($doctor);
|
||
|
||
$session = new PatientSession($record);
|
||
$this->em->persist($session);
|
||
$this->em->flush();
|
||
|
||
$invoice = new Invoice('doctor', $doctor->getId());
|
||
$invoice->setPatientSessionId($session->getId());
|
||
$this->em->persist($invoice);
|
||
$this->em->flush();
|
||
|
||
$res = $this->authJson('GET', '/api/v1/billing/invoices/' . $invoice->getUuid(), $owner);
|
||
self::assertSame(200, $this->responseCode());
|
||
|
||
$s = $res['data']['data']['session'];
|
||
self::assertSame([], $s['payments']);
|
||
self::assertSame([], $s['consumables']);
|
||
self::assertSame(0, $s['paid_total_rials']);
|
||
self::assertSame(0, $s['discount_rials']);
|
||
}
|
||
}
|