# 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`).
