test: cover the paths that were reasoned about but never executed

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>
This commit is contained in:
hamed
2026-08-01 15:16:26 +03:30
co-authored by Claude Opus 5
parent 5c754244f2
commit 2baa2ce7ca
4 changed files with 282 additions and 0 deletions
+48
View File
@@ -186,6 +186,54 @@ class ReportTest extends ApiTestCase
// ── بهره‌وری منابع ──────────────────────────────────────────────────────
/** منبعی بدون تقویم «۰٪ بهره‌وری» ندارد — بهره‌وری‌اش تعریف‌نشده است. */
/**
* ⭐ چهار آستانه، هر کدام روی مرز خودش.
*
* آستانه‌ای که یک درجه اشتباه بیفتد، یا همه‌چیز را قرمز می‌کند (و کسی دیگر نگاه
* نمی‌کند) یا هیچ‌چیز را (و گزارش بی‌فایده است).
*
* @param int $planned مدت برنامه
* @param int $actual مدت واقعی
*/
#[\PHPUnit\Framework\Attributes\DataProvider('severityCases')]
public function testEachSeverityThresholdIsHitExactly(int $planned, int $actual, string $expected): void
{
[$user, $section, $address, $doctor] = $this->clinic();
$service = $this->service($section, 'خدمت آستانه', $planned);
$patient = $this->createUser(['ROLE_USER']);
for ($i = 1; $i <= 3; $i++) {
$this->completed($doctor, $patient, $service, (int) $address->getClinicId(), $planned, $actual, $i);
}
$body = $this->authJson(
'GET',
sprintf('/api/v1/reports/plan-accuracy?from=%d&to=%d', time() - 10 * 86400, time()),
$user,
);
$row = $body['data']['rows'][0];
self::assertSame($expected, $row['severity'], sprintf(
'انحراف %d%%',
$row['deviation_percent'],
));
}
/** مرزها: ۳۰ · ۱۵ · ۵ درصد، روی قدر مطلق. */
public static function severityCases(): array
{
return [
'دقیقاً روی مرز high' => [100, 130, 'high'],
'یک قدم زیر high' => [100, 129, 'medium'],
'دقیقاً روی مرز medium' => [100, 115, 'medium'],
'یک قدم زیر medium' => [100, 114, 'low'],
'دقیقاً روی مرز low' => [100, 105, 'low'],
'یک قدم زیر low' => [100, 104, 'none'],
'کوتاه‌تر هم شمرده می‌شود' => [100, 70, 'high'],
];
}
public function testAResourceWithoutACalendarHasNullUtilization(): void
{
[$user, , $address] = $this->clinic();