Files
clinicpro/tests/Patient/SessionConsumableTest.php
T
hamedandClaude Opus 4.8 07222826c3 feat: port tauri create-service page as 3-step new-session wizard
Backend:
- Add session_at and inventory_package_id to patient_sessions, new
  session_consumables table (migration Version20260716102537)
- New SessionConsumable entity/repository mirroring SessionService;
  price snapshot, quantity >= 1, tenant-scoped silent skip
- PatientService::createSession accepts session_at, consumables[] and
  inventory_package_uuid; consumables are fully patient-paid (no
  insurance coverage) and added to final_price_rials
- Functional tests: success, foreign-tenant/unknown skip, empty and
  zero-quantity edges (tests/Patient/SessionConsumableTest.php)
- docs/api/patient.md updated for the new Create Session fields

Frontend (admin):
- NewSessionPage rewritten as the tauri /files/create-service 3-step
  wizard (ایجاد سرویس ← پرداخت ← جزییات) using SessionStepper
- New CreateStep: acceptance date/time (Jalali), section/service/staff,
  consumables with counters, package select, conditional insurance
  block (insured service or insured patient profile), price summary
- PaymentStep/DetailsStep extracted from SessionPaymentPage and shared
  between both pages (behavior unchanged, tests still green)
- UserTick and FilesServiceAddCard icons ported verbatim from tauri
- Vitest coverage for the wizard incl. empty-data states

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-16 14:20:32 +03:30

176 lines
7.8 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
namespace App\Tests\Patient;
use App\Doctor\Entity\Doctor;
use App\Inventory\Entity\InventoryItem;
use App\Inventory\Entity\InventoryPackage;
use App\Patient\Entity\PatientRecord;
use App\Tests\ApiTestCase;
/**
* ثبت مراجعه با کالای مصرفی/پکیج/زمان پذیرش (ویزارد ثبت مراجعه):
* POST /patient/{uuid}/session با consumables (سهم بیمار کامل، بدون بیمه)،
* inventory_package_uuid (مرجع) و session_at. آیتم‌های tenant دیگر بی‌صدا رد می‌شوند.
*/
class SessionConsumableTest extends ApiTestCase
{
/** @return array{0: \App\Auth\Entity\User, 1: Doctor, 2: PatientRecord} */
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, $doctor, $record];
}
private function itemFor(Doctor $doctor, string $name, int $price): InventoryItem
{
$item = new InventoryItem('doctor', $doctor->getId(), $name);
$item->setPrice($price)->setStock(100);
$this->em->persist($item);
$this->em->flush();
return $item;
}
// ── موفق ────────────────────────────────────────────────────────────────
public function testCreateSessionWithConsumablesPackageAndSessionAt(): void
{
[$owner, $doctor, $record] = $this->recordFor();
$glasses = $this->itemFor($doctor, 'عینک', 1_200_000);
$pencil = $this->itemFor($doctor, 'مداد سفید', 300_000);
$package = new InventoryPackage('doctor', $doctor->getId(), 'پکیج زیبایی');
$this->em->persist($package);
$this->em->flush();
$res = $this->authJson('POST', '/api/v1/patient/' . $record->getUuid() . '/session', $owner, [
'visit_price_rials' => 500_000,
'session_at' => 1_760_000_000,
'inventory_package_uuid' => $package->getUuid(),
'consumables' => [
['inventory_item_uuid' => $glasses->getUuid(), 'quantity' => 2],
['inventory_item_uuid' => $pencil->getUuid()],
],
]);
self::assertSame(201, $this->responseCode());
self::assertSame(1_760_000_000, $res['data']['session_at']);
self::assertSame($package->getUuid(), $res['data']['inventory_package_uuid']);
self::assertSame('پکیج زیبایی', $res['data']['inventory_package_title']);
self::assertCount(2, $res['data']['consumables']);
self::assertSame(2_700_000, $res['data']['consumables_total_rials']); // 2×1٬200٬000 + 300٬000
$byName = array_column($res['data']['consumables'], null, 'item_name');
self::assertSame(2, $byName['عینک']['quantity']);
self::assertSame(2_400_000, $byName['عینک']['line_total_rials']);
self::assertSame(1, $byName['مداد سفید']['quantity']);
// کالاها بدون پوشش بیمه → کامل روی سهم بیمار
self::assertSame(3_200_000, $res['data']['final_price_rials']); // ویزیت 500٬000 + کالاها 2٬700٬000
}
public function testConsumablesAppearInSessionsList(): void
{
[$owner, $doctor, $record] = $this->recordFor();
$item = $this->itemFor($doctor, 'سرنگ', 50_000);
$this->authJson('POST', '/api/v1/patient/' . $record->getUuid() . '/session', $owner, [
'visit_price_rials' => 0,
'consumables' => [['inventory_item_uuid' => $item->getUuid(), 'quantity' => 3]],
]);
self::assertSame(201, $this->responseCode());
$sessions = $this->authJson('GET', '/api/v1/patient/' . $record->getUuid() . '/sessions', $owner);
self::assertCount(1, $sessions['data'][0]['consumables']);
self::assertSame(150_000, $sessions['data'][0]['consumables_total_rials']);
self::assertSame(150_000, $sessions['data'][0]['patient_debt_rials']);
}
// ── خطا ─────────────────────────────────────────────────────────────────
public function testForeignTenantConsumableAndPackageSilentlySkipped(): void
{
[$owner, , $record] = $this->recordFor();
// tenant دیگر
$otherOwner = $this->createUser(['ROLE_DOCTOR']);
$otherDoctor = new Doctor($otherOwner, 'دکتر دیگر');
$this->em->persist($otherDoctor);
$this->em->flush();
$foreignItem = $this->itemFor($otherDoctor, 'آمپول', 900_000);
$foreignPackage = new InventoryPackage('doctor', $otherDoctor->getId(), 'پکیج دیگری');
$this->em->persist($foreignPackage);
$this->em->flush();
$res = $this->authJson('POST', '/api/v1/patient/' . $record->getUuid() . '/session', $owner, [
'visit_price_rials' => 400_000,
'inventory_package_uuid' => $foreignPackage->getUuid(),
'consumables' => [['inventory_item_uuid' => $foreignItem->getUuid(), 'quantity' => 5]],
]);
self::assertSame(201, $this->responseCode());
self::assertCount(0, $res['data']['consumables']);
self::assertSame(0, $res['data']['consumables_total_rials']);
self::assertNull($res['data']['inventory_package_uuid']);
self::assertSame(400_000, $res['data']['final_price_rials']);
}
public function testUnknownConsumableUuidSkipped(): void
{
[$owner, , $record] = $this->recordFor();
$res = $this->authJson('POST', '/api/v1/patient/' . $record->getUuid() . '/session', $owner, [
'visit_price_rials' => 200_000,
'consumables' => [['inventory_item_uuid' => '00000000-0000-0000-0000-000000000000']],
]);
self::assertSame(201, $this->responseCode());
self::assertCount(0, $res['data']['consumables']);
self::assertSame(200_000, $res['data']['final_price_rials']);
}
// ── مرزی ────────────────────────────────────────────────────────────────
public function testEmptyConsumablesAndNoSessionAtDefaults(): void
{
[$owner, , $record] = $this->recordFor();
$res = $this->authJson('POST', '/api/v1/patient/' . $record->getUuid() . '/session', $owner, [
'visit_price_rials' => 100_000,
'consumables' => [],
]);
self::assertSame(201, $this->responseCode());
self::assertSame([], $res['data']['consumables']);
self::assertSame(0, $res['data']['consumables_total_rials']);
self::assertNull($res['data']['session_at']);
self::assertNull($res['data']['inventory_package_uuid']);
}
public function testZeroQuantityCoercedToOne(): void
{
[$owner, $doctor, $record] = $this->recordFor();
$item = $this->itemFor($doctor, 'گاز استریل', 80_000);
$res = $this->authJson('POST', '/api/v1/patient/' . $record->getUuid() . '/session', $owner, [
'visit_price_rials' => 0,
'consumables' => [['inventory_item_uuid' => $item->getUuid(), 'quantity' => 0]],
]);
self::assertSame(201, $this->responseCode());
self::assertSame(1, $res['data']['consumables'][0]['quantity']);
self::assertSame(80_000, $res['data']['consumables_total_rials']);
}
}