feat(pricing): date-ranged price lists and immutable appointment invoices
Section 12 and the fifth closing rule: changing a price never changes an already-booked appointment. The pricing chain already existed and worked. Two things were missing. Tariff only carries a year, so a rate change starting in Mehr could not be expressed — PriceList now takes an explicit date range and Tariff remains the layer beneath it. And an appointment stored a single number, so after a price change or a discount nobody could say what those 2,400,000 rials were made of. Price resolution walks four layers per service and takes the first hit: branch override, then the covering price list, then the yearly tariff, then the service's own price. The last one is the guarantee that a date no list covers still returns a price rather than zero or an exception. breakdown.sources reports which layer answered, so a surprising number can be traced instead of guessed at. Two calculation decisions worth stating. Tax is computed on the patient's share, not the gross — a patient does not pay tax on the portion the insurer covers. And a discount larger than the amount floors the total at zero rather than going negative, because a negative balance would mean the clinic owes the patient money, which nothing downstream is built to mean. A branch-specific list deliberately does not count as overlapping a general one; it takes precedence instead. Treating them as a conflict would have made per-branch exceptions impossible to express. Lists have no effect until activated, so drafting next quarter's prices cannot disturb today's. PriceSnapshot has no setters and a unique key on appointment_id: a snapshot that can be edited is not a snapshot, and two invoices for one appointment would be two truths. Corrections are a new row plus voiding the old one. Invoices are written during confirm with the prices of that moment — computing later would let a rate change between booking and invoicing produce a different number, which is exactly what rule five forbids. 12 tests. The one that matters is testBookedAppointmentKeepsItsOriginalInvoiceAfterAPriceChange: book, double the service price, watch quote return the new number while the appointment's invoice returns the old one. Without it rule five is only a claim. 1220 tests / 3551 assertions. phpstan back at its 14-error baseline. Frozen slot contract green. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -88,6 +88,7 @@ Only **digits** are translated — no characters are stripped, so `IR` in a sheb
|
||||
| [appointment-plan.md](appointment-plan.md) | Appointment segments and plan preview | 3 |
|
||||
| [appointment-availability.md](appointment-availability.md) | Multi-resource availability search | 2 |
|
||||
| [appointment-booking.md](appointment-booking.md) | Holds, confirmation and multi-resource occupancy | 4 |
|
||||
| [pricing.md](pricing.md) | Date-ranged price lists and appointment invoices | 8 |
|
||||
| [appointment.md](appointment.md) | Appointments & slot booking | 6 |
|
||||
| [appointment-settings.md](appointment-settings.md) | Weekly schedule, date overrides, holidays | 14 |
|
||||
| [payment.md](payment.md) | Payments (Mellat / Sep) | 5 |
|
||||
|
||||
Reference in New Issue
Block a user