Fifteen rows across five tasks said the mechanism was there and the test was not. Each of these is a case where being wrong would be silent. - the price rows must add up to the final amount. The chain test checks every number individually, which stays green if a new row is added and left out of the total; this checks the relationship itself. - a fixed deposit beats a percentage one, and neither can exceed the final amount — charging a deposit larger than the bill puts the patient in debt before the visit. - an appointment booked without a service still gets an invoice. Slot mode has no service, and without this the financial report is short a row with nothing to say which. - the four accuracy thresholds, each tested on its own boundary. One step off and either everything is red (so nobody looks) or nothing is (so the report is pointless). Includes a short-running service, since the deviation is measured on its absolute value. - all six policy templates build a policy that survives the normal validation, simulation and activation path. A template is a shortcut, not a second road: if one of them produced something the validator rejects, a user could create a rule in one click that never works. - simulation leaves nothing pending for a later flush in the same request. That is what the finally-rollback-clear is for, and the failure would surface in the next operation rather than in the sandbox. The course controller was reading $this->credits without it being injected — phpstan caught it; the package-shortfall path had no test yet and would have 500'd on the first course that had a package. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
497 lines
22 KiB
PHP
497 lines
22 KiB
PHP
<?php
|
||
|
||
namespace App\Tests\Pricing;
|
||
|
||
use App\Auth\Entity\User;
|
||
use App\Clinic\Entity\Clinic;
|
||
use App\ClinicService\Entity\ServiceItem;
|
||
use App\ClinicService\Entity\ServiceSection;
|
||
use App\Doctor\Entity\DoctorAddress;
|
||
use App\Tests\ApiTestCase;
|
||
|
||
/**
|
||
* لیست قیمت بازهدار و فاکتور تفکیکشده — بند ۱۲ و قانون پنجم مستند.
|
||
*/
|
||
class PricingTest extends ApiTestCase
|
||
{
|
||
/** @return array{user: User, section: ServiceSection, address: DoctorAddress} */
|
||
private function clinic(): array
|
||
{
|
||
$user = $this->createUser(['ROLE_USER', 'ROLE_CLINIC']);
|
||
$clinic = new Clinic($user);
|
||
$clinic->setName('کلینیک قیمت');
|
||
$this->em->persist($clinic);
|
||
$this->em->flush();
|
||
|
||
$section = new ServiceSection('clinic', $clinic->getId(), 'زیبایی');
|
||
$this->em->persist($section);
|
||
|
||
$address = DoctorAddress::forClinic($clinic->getId());
|
||
$address->setName('شعبهٔ مرکزی');
|
||
$this->em->persist($address);
|
||
$this->em->flush();
|
||
|
||
return ['user' => $user, 'section' => $section, 'address' => $address];
|
||
}
|
||
|
||
private function service(ServiceSection $section, string $name, int $price): ServiceItem
|
||
{
|
||
$section = $this->em->getRepository(ServiceSection::class)->find($section->getId());
|
||
|
||
$item = new ServiceItem($section, $name);
|
||
$item->setPriceRials($price);
|
||
$item->setSoloDurationMinutes(20);
|
||
$this->em->persist($item);
|
||
$this->em->flush();
|
||
|
||
return $item;
|
||
}
|
||
|
||
/** @param array<string, mixed> $extra */
|
||
private function quote(User $user, ServiceItem $service, DoctorAddress $address, array $extra = []): array
|
||
{
|
||
return $this->authJson('POST', '/api/v1/pricing/quote', $user, $extra + [
|
||
'service_uuid' => $service->getUuid(),
|
||
'branch_uuid' => $address->getUuid(),
|
||
]);
|
||
}
|
||
|
||
private function priceList(User $user, string $name, int $from, int $to, ?string $addressUuid = null): array
|
||
{
|
||
$body = $this->authJson('POST', '/api/v1/price-lists', $user, array_filter([
|
||
'name' => $name,
|
||
'starts_at' => $from,
|
||
'ends_at' => $to,
|
||
'address_uuid' => $addressUuid,
|
||
]));
|
||
self::assertSame(201, $this->responseCode(), json_encode($body, JSON_UNESCAPED_UNICODE));
|
||
|
||
return $body['data'];
|
||
}
|
||
|
||
/** بدون هیچ لیست قیمتی، قیمت خودِ سرویس برمیگردد — هرگز صفر یا خطا. */
|
||
public function testFallsBackToTheServicePrice(): void
|
||
{
|
||
$c = $this->clinic();
|
||
$service = $this->service($c['section'], 'بوتاکس', 5_000_000);
|
||
|
||
$body = $this->quote($c['user'], $service, $c['address']);
|
||
|
||
self::assertSame(200, $this->responseCode(), json_encode($body, JSON_UNESCAPED_UNICODE));
|
||
self::assertSame(5_000_000, $body['data']['base_rials']);
|
||
self::assertSame(5_000_000, $body['data']['final_rials']);
|
||
self::assertSame('service_item', $body['data']['breakdown']['sources'][$service->getUuid()]);
|
||
}
|
||
|
||
/** لیست قیمت فقط در بازهٔ خودش حاکم است. */
|
||
public function testPriceListAppliesOnlyInsideItsRange(): void
|
||
{
|
||
$c = $this->clinic();
|
||
$service = $this->service($c['section'], 'بوتاکس', 5_000_000);
|
||
|
||
$from = strtotime('+10 days');
|
||
$to = strtotime('+40 days');
|
||
|
||
$list = $this->priceList($c['user'], 'نیمهٔ دوم', $from, $to);
|
||
|
||
$this->authJson('PUT', "/api/v1/price-list/{$list['uuid']}/items", $c['user'], [
|
||
'items' => [['service_uuid' => $service->getUuid(), 'price_rials' => 8_000_000]],
|
||
]);
|
||
self::assertSame(200, $this->responseCode());
|
||
|
||
$this->authJson('POST', "/api/v1/price-list/{$list['uuid']}/activate", $c['user']);
|
||
self::assertSame(200, $this->responseCode());
|
||
|
||
$inside = $this->quote($c['user'], $service, $c['address'], ['at' => $from + 86400]);
|
||
self::assertSame(8_000_000, $inside['data']['base_rials'], 'داخل بازه: قیمت جدید');
|
||
|
||
$before = $this->quote($c['user'], $service, $c['address'], ['at' => $from - 86400]);
|
||
self::assertSame(5_000_000, $before['data']['base_rials'], 'پیش از بازه: قیمت قبلی');
|
||
}
|
||
|
||
/** دو لیست فعالِ همپوشان یعنی یک تاریخ دو قیمت — هنگام فعالسازی رد میشود. */
|
||
public function testOverlappingActiveListsAreRejected(): void
|
||
{
|
||
$c = $this->clinic();
|
||
$from = strtotime('+10 days');
|
||
|
||
$first = $this->priceList($c['user'], 'اول', $from, $from + 30 * 86400);
|
||
$this->authJson('POST', "/api/v1/price-list/{$first['uuid']}/activate", $c['user']);
|
||
self::assertSame(200, $this->responseCode());
|
||
|
||
$second = $this->priceList($c['user'], 'دوم', $from + 10 * 86400, $from + 50 * 86400);
|
||
$body = $this->authJson('POST', "/api/v1/price-list/{$second['uuid']}/activate", $c['user']);
|
||
|
||
self::assertSame(422, $this->responseCode());
|
||
self::assertStringContainsString('همپوشانی', $body['errors'][0]['message']);
|
||
}
|
||
|
||
/** لیستِ یک شعبه با لیست عمومی تداخل ندارد و بر آن مقدم است. */
|
||
public function testBranchListWinsOverTheGeneralList(): void
|
||
{
|
||
$c = $this->clinic();
|
||
$service = $this->service($c['section'], 'بوتاکس', 5_000_000);
|
||
|
||
$from = strtotime('+10 days');
|
||
$to = $from + 30 * 86400;
|
||
|
||
$general = $this->priceList($c['user'], 'عمومی', $from, $to);
|
||
$this->authJson('PUT', "/api/v1/price-list/{$general['uuid']}/items", $c['user'], [
|
||
'items' => [['service_uuid' => $service->getUuid(), 'price_rials' => 7_000_000]],
|
||
]);
|
||
$this->authJson('POST', "/api/v1/price-list/{$general['uuid']}/activate", $c['user']);
|
||
self::assertSame(200, $this->responseCode());
|
||
|
||
$branch = $this->priceList($c['user'], 'شعبهٔ مرکزی', $from, $to, $c['address']->getUuid());
|
||
$this->authJson('PUT', "/api/v1/price-list/{$branch['uuid']}/items", $c['user'], [
|
||
'items' => [['service_uuid' => $service->getUuid(), 'price_rials' => 9_000_000]],
|
||
]);
|
||
$this->authJson('POST', "/api/v1/price-list/{$branch['uuid']}/activate", $c['user']);
|
||
self::assertSame(200, $this->responseCode(), 'لیست شعبه با لیست عمومی تداخل ندارد');
|
||
|
||
$body = $this->quote($c['user'], $service, $c['address'], ['at' => $from + 86400]);
|
||
|
||
self::assertSame(9_000_000, $body['data']['base_rials']);
|
||
}
|
||
|
||
/** override شعبه (تسک ۰۴) بر لیست قیمت مقدم است. */
|
||
public function testBranchOverrideBeatsThePriceList(): void
|
||
{
|
||
$c = $this->clinic();
|
||
$service = $this->service($c['section'], 'بوتاکس', 5_000_000);
|
||
|
||
$from = strtotime('+10 days');
|
||
$list = $this->priceList($c['user'], 'عمومی', $from, $from + 30 * 86400);
|
||
$this->authJson('PUT', "/api/v1/price-list/{$list['uuid']}/items", $c['user'], [
|
||
'items' => [['service_uuid' => $service->getUuid(), 'price_rials' => 7_000_000]],
|
||
]);
|
||
$this->authJson('POST', "/api/v1/price-list/{$list['uuid']}/activate", $c['user']);
|
||
|
||
$this->authJson('PUT', "/api/v1/service-item/{$service->getUuid()}/branch-overrides", $c['user'], [
|
||
'overrides' => [['address_uuid' => $c['address']->getUuid(), 'price_rials' => 11_000_000]],
|
||
]);
|
||
self::assertSame(200, $this->responseCode());
|
||
|
||
$body = $this->quote($c['user'], $service, $c['address'], ['at' => $from + 86400]);
|
||
|
||
self::assertSame(11_000_000, $body['data']['base_rials']);
|
||
self::assertSame('branch_override', $body['data']['breakdown']['sources'][$service->getUuid()]);
|
||
}
|
||
|
||
public function testFullChainAppliesInOrder(): void
|
||
{
|
||
$c = $this->clinic();
|
||
$service = $this->service($c['section'], 'لیزر', 10_000_000);
|
||
$extra = $this->service($c['section'], 'ناحیهٔ اضافه', 2_000_000);
|
||
|
||
$body = $this->quote($c['user'], $service, $c['address'], [
|
||
'item_uuids' => [$extra->getUuid()],
|
||
'policy' => [
|
||
'discount_percent' => 10,
|
||
'insurance_base_percent' => 20,
|
||
'insurance_supplementary_percent' => 50,
|
||
'tax_percent' => 10,
|
||
'deposit_percent' => 30,
|
||
],
|
||
]);
|
||
|
||
$d = $body['data'];
|
||
|
||
self::assertSame(10_000_000, $d['base_rials']);
|
||
self::assertSame(2_000_000, $d['items_rials']);
|
||
self::assertSame(1_200_000, $d['discount_rials'], '۱۰٪ از ۱۲ میلیون');
|
||
self::assertSame(2_160_000, $d['insurance_base_rials'], '۲۰٪ از ۱۰٫۸ میلیون');
|
||
self::assertSame(4_320_000, $d['insurance_supplementary_rials'], '۵۰٪ از باقیمانده');
|
||
self::assertSame(432_000, $d['tax_rials'], '۱۰٪ روی سهم بیمار، نه روی کل');
|
||
self::assertSame(4_752_000, $d['final_rials']);
|
||
self::assertSame(1_425_600, $d['deposit_rials']);
|
||
}
|
||
|
||
/** تخفیف بیشتر از مبلغ، مبلغ را صفر میکند نه منفی. */
|
||
public function testDiscountLargerThanTheAmountFloorsAtZero(): void
|
||
{
|
||
$c = $this->clinic();
|
||
$service = $this->service($c['section'], 'ویزیت', 1_000_000);
|
||
|
||
$body = $this->quote($c['user'], $service, $c['address'], [
|
||
'policy' => ['discount_rials' => 5_000_000],
|
||
]);
|
||
|
||
self::assertSame(0, $body['data']['final_rials']);
|
||
self::assertGreaterThanOrEqual(0, $body['data']['discount_rials']);
|
||
}
|
||
|
||
/** سقف جمع تخفیفها per محیط اعمال میشود. */
|
||
public function testTotalDiscountCapIsApplied(): void
|
||
{
|
||
$c = $this->clinic();
|
||
$service = $this->service($c['section'], 'ویزیت', 10_000_000);
|
||
|
||
$body = $this->quote($c['user'], $service, $c['address'], [
|
||
'policy' => [
|
||
'discount_percent' => 40,
|
||
'discount_rials' => 3_000_000,
|
||
'max_total_discount_percent' => 25,
|
||
],
|
||
]);
|
||
|
||
self::assertSame(2_500_000, $body['data']['discount_rials'], 'سقف ۲۵٪ از ۱۰ میلیون');
|
||
self::assertSame(7_500_000, $body['data']['final_rials']);
|
||
}
|
||
|
||
/**
|
||
* ⭐ جمعِ ردیفها **باید** برابر مبلغ نهایی باشد.
|
||
*
|
||
* تست زنجیره عددها را تکتک میسنجد؛ این یکی خودِ رابطه را میسنجد. اگر روزی ردیف
|
||
* تازهای اضافه شود و در جمع نهایی حساب نشود، آن تست سبز میماند و این قرمز.
|
||
*/
|
||
public function testTheRowsAlwaysAddUpToTheFinalAmount(): void
|
||
{
|
||
$c = $this->clinic();
|
||
$service = $this->service($c['section'], 'لیزر', 9_500_000);
|
||
$extra = $this->service($c['section'], 'ناحیهٔ اضافه', 3_300_000);
|
||
|
||
$d = $this->quote($c['user'], $service, $c['address'], [
|
||
'item_uuids' => [$extra->getUuid()],
|
||
'policy' => [
|
||
'discount_percent' => 7,
|
||
'insurance_base_percent' => 15,
|
||
'insurance_supplementary_percent' => 25,
|
||
'tax_percent' => 9,
|
||
],
|
||
])['data'];
|
||
|
||
$sum = $d['base_rials']
|
||
+ $d['items_rials']
|
||
- $d['discount_rials']
|
||
- $d['insurance_base_rials']
|
||
- $d['insurance_supplementary_rials']
|
||
+ $d['tax_rials'];
|
||
|
||
self::assertSame($d['final_rials'], $sum, json_encode($d, JSON_UNESCAPED_UNICODE));
|
||
}
|
||
|
||
/** بیعانهٔ مبلغی بر درصدی مقدم است و هرگز از مبلغ نهایی بیشتر نمیشود. */
|
||
public function testAFixedDepositOverridesThePercentageAndIsCapped(): void
|
||
{
|
||
$c = $this->clinic();
|
||
$service = $this->service($c['section'], 'ویزیت', 2_000_000);
|
||
|
||
$fixed = $this->quote($c['user'], $service, $c['address'], [
|
||
'policy' => ['deposit_percent' => 50, 'deposit_rials' => 300_000],
|
||
])['data'];
|
||
|
||
self::assertSame(300_000, $fixed['deposit_rials'], 'مبلغی برنده است، نه ۵۰٪');
|
||
|
||
$capped = $this->quote($c['user'], $service, $c['address'], [
|
||
'policy' => ['deposit_rials' => 9_000_000],
|
||
])['data'];
|
||
|
||
self::assertSame(
|
||
$capped['final_rials'],
|
||
$capped['deposit_rials'],
|
||
'بیعانهٔ بیشتر از مبلغ نهایی یعنی بدهکار کردن بیمار پیش از ویزیت',
|
||
);
|
||
}
|
||
|
||
public function testForeignServiceIsNotFound(): void
|
||
{
|
||
$c = $this->clinic();
|
||
$other = $this->clinic();
|
||
$foreign = $this->service($other['section'], 'سرویس بیگانه', 1_000_000);
|
||
|
||
$this->quote($c['user'], $foreign, $c['address']);
|
||
|
||
self::assertSame(404, $this->responseCode());
|
||
}
|
||
|
||
public function testNegativePriceIsRejected(): void
|
||
{
|
||
$c = $this->clinic();
|
||
$service = $this->service($c['section'], 'ویزیت', 1_000_000);
|
||
$list = $this->priceList($c['user'], 'تست', strtotime('+1 day'), strtotime('+30 days'));
|
||
|
||
$body = $this->authJson('PUT', "/api/v1/price-list/{$list['uuid']}/items", $c['user'], [
|
||
'items' => [['service_uuid' => $service->getUuid(), 'price_rials' => -100]],
|
||
]);
|
||
|
||
self::assertSame(422, $this->responseCode());
|
||
self::assertSame('price_rials', $body['errors'][0]['field']);
|
||
}
|
||
|
||
/**
|
||
* ⭐ قانون پنجم مستند: «تغییر قیمت هرگز نوبتهای ثبتشده را عوض نمیکند.»
|
||
*
|
||
* نوبت ثبت میشود، بعد قیمت سرویس دو برابر میشود، و فاکتور همان اعداد قبلی را
|
||
* میدهد. بدون این تست، تسک تأییدشده نیست.
|
||
*/
|
||
public function testBookedAppointmentKeepsItsOriginalInvoiceAfterAPriceChange(): void
|
||
{
|
||
$c = $this->clinic();
|
||
$service = $this->service($c['section'], 'لیزر', 4_000_000);
|
||
|
||
$room = new \App\Resource\Entity\ResourceType(
|
||
$c['address']->tenantEntityType(),
|
||
$c['address']->tenantEntityId(),
|
||
'room',
|
||
'اتاق',
|
||
);
|
||
$this->em->persist($room);
|
||
$this->em->flush();
|
||
|
||
$resource = $this->authJson('POST', '/api/v1/resource', $c['user'], [
|
||
'address_uuid' => $c['address']->getUuid(),
|
||
'type_uuid' => $room->getUuid(),
|
||
'name' => 'اتاق ۱',
|
||
]);
|
||
self::assertSame(201, $this->responseCode());
|
||
|
||
$this->authJson('PUT', "/api/v1/resource/{$resource['data']['uuid']}/calendar", $c['user'], [
|
||
'days' => array_fill_keys(range(0, 6), [['start_minute' => 0, 'end_minute' => 1440]]),
|
||
]);
|
||
|
||
$this->authJson('PUT', "/api/v1/service-item/{$service->getUuid()}/segments", $c['user'], [
|
||
'segments' => [[
|
||
'sequence' => 1, 'name' => 'لیزر', 'duration_minutes' => 20,
|
||
'requirements' => [['type_uuid' => $room->getUuid()]],
|
||
]],
|
||
]);
|
||
self::assertSame(200, $this->responseCode());
|
||
|
||
$doctorUser = $this->createUser(['ROLE_USER', 'ROLE_DOCTOR']);
|
||
$doctor = new \App\Doctor\Entity\Doctor($doctorUser, 'دکتر فاکتور');
|
||
$this->em->persist($doctor);
|
||
$this->em->flush();
|
||
|
||
$start = (new \DateTimeImmutable('next saturday', new \DateTimeZone('Asia/Tehran')))
|
||
->setTime(9, 0)
|
||
->getTimestamp();
|
||
|
||
$hold = $this->authJson('POST', '/api/v1/appointment-hold', $c['user'], [
|
||
'service_uuid' => $service->getUuid(),
|
||
'branch_uuid' => $c['address']->getUuid(),
|
||
'start' => $start,
|
||
'assignment' => ['room' => [$resource['data']['uuid']]],
|
||
]);
|
||
self::assertSame(201, $this->responseCode(), json_encode($hold, JSON_UNESCAPED_UNICODE));
|
||
|
||
$confirmed = $this->authJson('POST', '/api/v1/appointment-confirm', $c['user'], [
|
||
'hold_uuid' => $hold['data']['hold_uuid'],
|
||
'doctor_uuid' => $doctor->getUuid(),
|
||
'service_uuid' => $service->getUuid(),
|
||
'branch_uuid' => $c['address']->getUuid(),
|
||
]);
|
||
self::assertSame(200, $this->responseCode(), json_encode($confirmed, JSON_UNESCAPED_UNICODE));
|
||
self::assertSame(4_000_000, $confirmed['data']['price_snapshot']['final_rials']);
|
||
|
||
$appointmentUuid = $confirmed['data']['appointment_uuid'];
|
||
|
||
// حالا قیمت دو برابر میشود.
|
||
$this->em->clear();
|
||
$reloaded = $this->em->getRepository(ServiceItem::class)->findOneBy(['uuid' => $service->getUuid()]);
|
||
$reloaded->setPriceRials(8_000_000);
|
||
$this->em->flush();
|
||
|
||
// قیمت جدید در quote دیده میشود…
|
||
$fresh = $this->quote($c['user'], $reloaded, $c['address']);
|
||
self::assertSame(8_000_000, $fresh['data']['final_rials']);
|
||
|
||
// …ولی فاکتور نوبتِ ثبتشده دستنخورده است.
|
||
$snapshot = $this->authJson('GET', "/api/v1/appointment/$appointmentUuid/price-snapshot", $c['user']);
|
||
|
||
self::assertSame(200, $this->responseCode(), json_encode($snapshot, JSON_UNESCAPED_UNICODE));
|
||
self::assertSame(4_000_000, $snapshot['data']['final_rials'], 'قانون پنجم: فاکتور ثبتشده عوض نمیشود');
|
||
self::assertSame(4_000_000, $snapshot['data']['base_rials']);
|
||
}
|
||
|
||
/**
|
||
* ⭐ نوبت بدون سرویس هم فاکتور میگیرد.
|
||
*
|
||
* حالت اسلاتی سرویس ندارد؛ اگر فاکتورش ساخته نمیشد، گزارش مالی یک ردیف کم داشت و
|
||
* هیچجا هم معلوم نمیشد کدام ردیف.
|
||
*/
|
||
public function testAnAppointmentWithoutAServiceStillGetsAnInvoice(): void
|
||
{
|
||
$c = $this->clinic();
|
||
$service = $this->service($c['section'], 'ویزیت', 1_500_000);
|
||
|
||
$room = new \App\Resource\Entity\ResourceType(
|
||
$c['address']->tenantEntityType(),
|
||
$c['address']->tenantEntityId(),
|
||
'room',
|
||
'اتاق',
|
||
);
|
||
$this->em->persist($room);
|
||
$this->em->flush();
|
||
|
||
$resource = $this->authJson('POST', '/api/v1/resource', $c['user'], [
|
||
'address_uuid' => $c['address']->getUuid(),
|
||
'type_uuid' => $room->getUuid(),
|
||
'name' => 'اتاق ویزیت',
|
||
]);
|
||
self::assertSame(201, $this->responseCode());
|
||
|
||
$this->authJson('PUT', "/api/v1/resource/{$resource['data']['uuid']}/calendar", $c['user'], [
|
||
'days' => array_fill_keys(range(0, 6), [['start_minute' => 0, 'end_minute' => 1440]]),
|
||
]);
|
||
|
||
$this->authJson('PUT', "/api/v1/service-item/{$service->getUuid()}/segments", $c['user'], [
|
||
'segments' => [[
|
||
'sequence' => 1, 'name' => 'ویزیت', 'duration_minutes' => 15,
|
||
'requirements' => [['type_uuid' => $room->getUuid()]],
|
||
]],
|
||
]);
|
||
self::assertSame(200, $this->responseCode());
|
||
|
||
$doctorUser = $this->createUser(['ROLE_USER', 'ROLE_DOCTOR']);
|
||
$doctor = new \App\Doctor\Entity\Doctor($doctorUser, 'دکتر ویزیت');
|
||
$this->em->persist($doctor);
|
||
$this->em->flush();
|
||
|
||
$start = (new \DateTimeImmutable('next saturday', new \DateTimeZone('Asia/Tehran')))
|
||
->setTime(11, 0)
|
||
->getTimestamp();
|
||
|
||
$hold = $this->authJson('POST', '/api/v1/appointment-hold', $c['user'], [
|
||
'service_uuid' => $service->getUuid(),
|
||
'branch_uuid' => $c['address']->getUuid(),
|
||
'start' => $start,
|
||
'assignment' => ['room' => [$resource['data']['uuid']]],
|
||
]);
|
||
self::assertSame(201, $this->responseCode(), json_encode($hold, JSON_UNESCAPED_UNICODE));
|
||
|
||
// ثبت **بدون** `service_uuid`: مسیر فاکتور تخت.
|
||
$confirmed = $this->authJson('POST', '/api/v1/appointment-confirm', $c['user'], [
|
||
'hold_uuid' => $hold['data']['hold_uuid'],
|
||
'doctor_uuid' => $doctor->getUuid(),
|
||
]);
|
||
self::assertSame(200, $this->responseCode(), json_encode($confirmed, JSON_UNESCAPED_UNICODE));
|
||
|
||
$snapshot = $this->authJson(
|
||
'GET',
|
||
"/api/v1/appointment/{$confirmed['data']['appointment_uuid']}/price-snapshot",
|
||
$c['user'],
|
||
);
|
||
|
||
self::assertSame(200, $this->responseCode(), 'فاکتور باید وجود داشته باشد');
|
||
self::assertSame(0, $snapshot['data']['items_rials']);
|
||
self::assertSame($snapshot['data']['base_rials'], $snapshot['data']['final_rials']);
|
||
}
|
||
|
||
public function testDraftListHasNoEffectUntilActivated(): void
|
||
{
|
||
$c = $this->clinic();
|
||
$service = $this->service($c['section'], 'بوتاکس', 5_000_000);
|
||
|
||
$from = strtotime('+2 days');
|
||
$list = $this->priceList($c['user'], 'پیشنویس', $from, $from + 30 * 86400);
|
||
$this->authJson('PUT', "/api/v1/price-list/{$list['uuid']}/items", $c['user'], [
|
||
'items' => [['service_uuid' => $service->getUuid(), 'price_rials' => 9_999_999]],
|
||
]);
|
||
|
||
$body = $this->quote($c['user'], $service, $c['address'], ['at' => $from + 86400]);
|
||
|
||
self::assertSame(5_000_000, $body['data']['base_rials'], 'پیشنویس نباید قیمت را عوض کند');
|
||
}
|
||
}
|