Allotment
Allotment is the per-date inventory a supplier grants Volāre on each service rate (supplier_service_rates.allotment). AllotmentService resolves which rates an offer/booking consumes, checks remaining capacity, and consumes/releases units as bookings are finalized or cancelled.
When to Use
Section titled “When to Use”- Deciding whether an offer is bookable on the web (
getOfferAvailability/ bulkgetOffersAvailability) - Booking finalization —
consumeAllotment()is called insideBookingFinalizationService’s transaction - Admin offer preview — the availability breakdown shown per service (
OfferPreviewData)
Not for date-range blackouts — that is Stop Sales (see below).
How Rates Are Resolved
Section titled “How Rates Are Resolved”resolveServiceRates(Offer, array $session) maps an offer (plus any checkout upsell overrides in the session) to the SupplierServiceRate rows it consumes, each with the list of calendar service_dates:
- Hotels emit one consumed date per stay night — each consecutive itinerary day at the hotel becomes
departure_date + (day − 1). Checkout hotel upgrades replace the base hotel for the covered night range. - Activities and transfers emit one date per itinerary day they sit on; upsell selections on a day replace that day’s included/base entries. A trip-level luxury transfer maps to day 1 (the departure date).
- Package services (when the tour has them) emit a single date equal to the offer’s departure date.
The rate selected for each service is the one whose window covers departure_date (SupplierService::getRateForDate()). Multi-night stays do NOT re-resolve per night, even if the stay spills into a neighbouring rate window — this deliberately matches the pricing assumption.
Availability
Section titled “Availability”Consumption is tracked as one AllotmentConsumption row per (booking, rate, service_date). Remaining capacity for an offer is:
MIN over every (rate × service_date) of: rate.allotment − consumed_countgetOfferAvailability(Offer) returns an AllotmentAvailabilityResult DTO: available (the MIN), isAvailable, the bottleneck service name, and a per-service breakdown. Offers with no supplier tour rate or no constrained services are unlimited().
getOffersAvailability(Collection) is the bulk variant for product pages (all offers must share a tour; base services only, no upsells) — a few bulk queries instead of N per offer. Used by ProductByMarketController.
Consumption & Release
Section titled “Consumption & Release”consumeAllotment(Booking, array $session) — must run inside a DB transaction (it does, in BookingFinalizationService):
- Locks all involved rate rows with
lockForUpdate(), ordered by id (deterministic — no deadlocks), preventing concurrent overbooking - Pre-checks every
(rate, service_date)tuple and throwsInsufficientAllotmentExceptionbefore writing anything if one is already full - Inserts the consumption rows (
firstOrCreate, so re-finalization is idempotent) - When this booking takes the last unit of any
(rate, date), anAllotmentExhaustedNotificationemail goes to the Volāre entity address after commit (plus configured copies — see Email Copies)
releaseAllotment(Booking) deletes the booking’s consumption rows. It fires automatically from the Booking model’s updated hook whenever a booking’s status changes to cancelled (backend/app/Models/Booking.php).
Campaign Seat Quota vs Allotment
Section titled “Campaign Seat Quota vs Allotment”Last Seats campaigns carry a second, unrelated counter. It looks like allotment and is deliberately not the same thing:
| Allotment | Last Seats quota | |
|---|---|---|
| Counts | Bookings (one row per booking × rate × service date) | Travellers (a party of three takes three seats) |
| Granularity | Service rate × calendar date | Departure date of a product (all airports, all room types) |
| What it limits | Whether the trip can be sold at all | Whether the trip is sold at the campaign price |
| When exhausted | The offer is unavailable | The departure returns to its normal price and keeps selling |
| Partial fit | Not possible — the booking is refused | The party pays the campaign price for the seats left and the normal price for the rest |
| Stored on | allotment_consumptions |
last_seats_seat_consumptions (with the discount frozen) |
The quota is a commercial subset of the seats the supplier already contracted, never extra inventory — so it can never make a date unsellable.
Both are consumed in the same locked transaction in BookingFinalizationService::finalizeFromCheckoutSession(): AllotmentService::consumeAllotment() first, then CampaignSeatService::consume(), which locks its departure row with lockForUpdate() the same way. Both are released from the Booking model’s updated hook when a booking is cancelled.
The one asymmetry: a quota that ran short between payment and finalization does not fail the booking. The seat physically exists (allotment was checked separately), so the shortfall is recorded and logged rather than thrown.
Allotment vs Stop Sales
Section titled “Allotment vs Stop Sales”| Allotment | Stop sale | |
|---|---|---|
| Granularity | Per service rate × calendar date | Whole tour × date window |
| Semantics | Counted capacity, consumed booking by booking | Binary blackout, ignores capacity |
| Recovers | When bookings are cancelled (release) | When the window expires or is deleted |
| Managed as | allotment integer on SupplierServiceRate |
StopSale rows on the tour |
Both gates apply independently — an offer must have remaining allotment AND no active stop sale to be bookable.
Business Rules
Section titled “Business Rules”- Availability is always the worst
(rate, date)tuple — one exhausted hotel night blocks the whole offer - The departure-date rate governs the whole stay; nights never split across rate windows
- Overbooking is prevented with pessimistic locks at finalization, not just the earlier availability check
- Cancelling a booking returns its units automatically
Related
Section titled “Related”- Stop Sales — tour-level date blackouts (the other bookability gate)
- Last Seats — the campaign seat quota consumed alongside allotment
- Booking Lifecycle — where consumption happens (finalization)
- Suppliers — services and rates data model
- Source:
backend/app/Services/Allotment/AllotmentService.php - Source:
backend/app/Services/Allotment/AllotmentAvailabilityResult.php - Source:
backend/app/Models/SupplierServiceRate.php