feat: enrich invoice summary with session payments, consumables and discount
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>
This commit is contained in:
@@ -0,0 +1,171 @@
|
||||
<?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']);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user