Skip to main content

Allocation Engine

The allocation engine turns an approved demand into a concrete assignment of groups → hotels with explicit, auditable reasoning for every decision.

How allocation runs

When demand is approved, a job lands on nexa-allocation:

1. Preprocess groups
- Validate tier consistency
- Split mixed-tier groups → MANUAL_REVIEW
2. For each tier:
2a. Search providers in parallel (Amadeus, Hotelbeds, Contracts)
2b. Deduplicate across providers (3-layer strategy)
2c. Filter by policy (stars, distance, price, amenities)
2d. Score candidates (cost dominant + distance + consolidation)
2e. Assign groups respecting integrity + capacity
3. Persist an AllocationWave
- All candidates, scores, reasons
- Final assignments + total cost
- Savings vs market for direct contracts
4. Enqueue booking

Ranking inputs

The scorer is deterministic and transparent. It weights three objectives:

ObjectiveDefault weightDirection
Cost (nightly rate × rooms × nights)0.60Lower is better
Distance from airport0.25Lower is better
Consolidation (groups placed in same hotel)0.15Higher is better

Amenities and stars act as gates (policy constraints), not as scoring inputs. Once a hotel passes the gate it competes purely on cost-distance-consolidation.

You can tune weights per policy with allocationWeights: { cost, distance, consolidation }.

Deduplication across providers

A hotel can surface from Amadeus, Hotelbeds, and as a direct contract simultaneously. Nexa picks one offer per hotel using:

  1. Provider-id match: if two offers share a resolved provider-independent chain/hotel ID.
  2. Geo+stars match: coordinates within 100m and same star rating.
  3. Name+address match: exact string match as fallback.

Once deduplicated, Nexa picks the cheapest offer — with a tiebreaker that prefers direct contracts (both for rate certainty and for the inventory bump you get back on confirmation).

Group integrity

Groups never split across hotels by default. If an assignment cannot fit a group at any policy-compliant hotel, the group is held — not broken — and lands in MANUAL_REVIEW with category ALLOCATION_FAILED.

Operators can explicitly override with a split decision (see Cases).

Tier separation

Tiers are scored independently and inventory is tracked between them:

Pass 1 (First / Business)  → consumes inventory at 4★+ hotels within 15 km
Pass 2 (Premium Economy) → sees post-Pass-1 availability, its own policy
Pass 3 (Economy) → sees post-Pass-2 availability, its own policy
Pass 4 (Crew) → per crew policy, usually pre-contracted

This matters because the 4★ hotel near the airport may sell out to Business before Economy even runs. It also prevents the system from sending a Business passenger to a 2★ property just because Economy claimed inventory first.

Overriding an allocation

Operators with OPS_SUPERVISOR or ADMIN can override an allocation:

curl -X POST https://us-central1.api.nexastudio.io/allocation/wave/$WAVE_URN/override \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"groupId": "GRP-4",
"hotelUrn": "urn:nexa:hotel:abc",
"reason": "passenger requested family-friendly hotel"
}'

The override:

  • Is recorded in audit with the actor, reason, and before/after state.
  • Triggers re-booking if a reservation already exists elsewhere.
  • Does not affect other groups' allocations.

Failure modes

SymptomLikely causeNext step
ALLOCATION_FAILED immediatelyNo hotels in the search radiusBroaden the policy radius temporarily or unblock contract inventory
All candidates filtered outAmenities too strict, stars too highRelax policy (versioned) or escalate via the exception agent
Wave completes but some groups HELDCapacity constraints, mixed tier groupsSplit or manually override; use POST /allocation/wave/:urn/rerun after
Huge delta between estimated and actual costCached offers staleForce refresh: POST /hotels/cache/invalidate?airport=XXX then rerun

Inspecting a wave

curl https://us-central1.api.nexastudio.io/allocation/wave/$WAVE_URN \
-H "Authorization: Bearer $TOKEN" | jq '.rankingInputs, .allocations'

Every rankingInput entry captures the candidate that was evaluated, the raw scores, the final weighted score, and the reason it was selected or discarded. This is the audit trail the ops team hands to finance when explaining "why did we book this hotel and not the cheaper one?"

Was this helpful?