# Priority & Batch Window Flow (Import, Freight) Export = no batch, no priority. Pure first-come-first-served (`booking-batch.service.ts:462-467, 625-628`). Everything below is import only. ## Step by step **1. Booking submitted → priority score computed** `booking-transition.service.ts:110-111,196-197` → `booking-pricing.service.ts:403-407` `computeSubmitPriorityScore()` → `rule-engine.service.ts:118`. - Government booking: `+50,000` (`government-priority.constants.ts:2`, applied `rule-engine.service.ts:201`) - Plus cargo/weight modifiers - Stored on `booking.priorityScore` **2. Window opens (PRE_WINDOW → OPEN)** Cron tick every 10s: `booking-window.service.ts:63` → `advanceImport` → `booking-window.service.ts:232-251`. Times computed by `computeImportWindowTimes` (`batch-window.util.ts:248-283`). **3. Customers book during OPEN** Booking lands as: - Commercial → `FULLY_EXECUTED` - Government → `APPROVED/PAID` (skips contract flow) **4. Window closes (OPEN → DOC_REVIEW)** `booking-window.service.ts:254-268`. Staff review docs for `docReviewMinutes`. **5. Doc review ends** Staff `completeDocReview()` (`booking-window.service.ts:124-159`) or timeout → `booking-window.service.ts:270-295`. Before batch runs: `expireUnacceptedForRouteDay` (`booking-batch.service.ts:1853-1883`) kills never-accepted bookings so they can't compete. **6. Batch fill runs** `processRouteDay` → `fillRouteDay` (`booking-batch.service.ts:1138-1319`), or single-schedule `fillSchedule` (`:1018-1128`). - Pool pulled pre-sorted: `findBatchPool`/`findBatchPoolByCorridorDay` (`bookings.repository.ts:991-1008, 1055-1083`) `ORDER BY is_government DESC, priority_score DESC, fully_executed_at ASC, created_at ASC` - Consolidated pairs grouped as one atomic unit: `groupConsolidatedPool` (`:1962-1987`) — never split. - Greedy placement, earliest-departing fitting train first: loop at `:1218-1306`. - No fit + government booking → `preemptForGovernment` (`:1891-1910`): bumps lowest-`priorityScore` commercial victim first, only if legs overlap (`:1920`). - No fit + commercial import (GENERAL/ONE_TIME) → maybe partial "split" offer: `maybeOfferPartial`/`isSplitEligible` (`:1326-1370`). - Still no fit → stays pooled, `notifier.unplaced` (`:1278-1280`). **7. Placed bookings get reserved/allocated** - Commercial: `reserve()` (`:1673-1703`) → `SELECTED_FOR_BATCH`, payment deadline set, DOC_REVIEW→PAYMENT (`booking-window.service.ts:275-294`). - Government: `allocate()` directly (`:1706-1746`), no payment step. **8. Payment phase ends** `booking-window.service.ts:297-309` → `settleDueReservations` → `settleReserved` (`:1437-1491`): - paid → allocated - unpaid → expired, capacity freed Then `concludeCycle` (`:315-373`): - Train full → `DONE` + auto-finalize (`:320-329`) - Not full → reopen same/next day (`nextCycleOpensAt` / office hours, `:331-372`, `batch-window.util.ts:217-224`) or `DONE` if no cycle fits before departure. **9. Backstop** `settleOverdueReservations` (`booking-window.service.ts:388-406`) catches any reservation whose deadline passed outside the normal tick. ## Phase enum `PRE_WINDOW → OPEN → DOC_REVIEW → PAYMENT → (reopen PRE_WINDOW | DONE)` (`booking-window.config.ts:27-34`) ## What decides priority 1. `is_government` — always first, both in SQL sort and `compareSchedulingPriority` util (`compare-scheduling-priority.util.ts:9-23`) 2. `priority_score` DESC (rule engine: government bonus + cargo/weight modifiers) 3. `fully_executed_at` ASC (earlier wins) 4. `created_at` ASC ## Edge cases - Government preemption only bumps if legs overlap; picks lowest-priority victim first. - Consolidated pairs are both-or-neither, never split (`:1326-1334, 1793`). - Only GENERAL/ONE_TIME import bookings are eligible for partial "split" offers. - Per-unit try/catch around reserve — one failure can't cause silent trickle/stagger allocation (comment at `:1283-1288`). - Each train freezes its own rule snapshot at window-open time, not live config (`booking-window.service.ts:85-93`).