MigrationContract — Schema¶
Status: ✅ Shipped — the pioneer migration-contract ledger is live: broker a cohort at a population hub's Pioneer Office, ferry pioneers into ship cargo in batches, settle them on frontier worlds. Distinct from the general trade-contract system (see
../FEATURES/economy/contracts.md) and from the planet-surplus pioneer sale (see../FEATURES/economy/lifecycle.md§1.4) — this page covers only the tracked-cohort ferry ledger.
Tracks a player's brokered cohort of pioneers from the population-hub Pioneer Office through delivery to a frontier world. Companion docs: ../FEATURES/planets/colonization.md (the narrative flow), ../FEATURES/economy/lifecycle.md (the separate planet-surplus sale faucet).
MigrationContract¶
Source: services/gameserver/src/models/migration_contract.py; services/gameserver/src/services/pioneer_service.py; services/gameserver/src/api/routes/pioneer.py.
Canon deviation (deliberate): canon places the Pioneer Office at the Capital Sector's Class-0 station (reached by docking); this contract layer instead surfaces it on the population-hub planet (landed). The station's colonist-commodity buy flow is untouched and still works independently.
Purpose: A contract is a tracked cohort — the player brokers N pioneers at a locked per-pioneer fee, ferries them from the hub into ship cargo in cargo-limited batches (loaded), and settles them on frontier worlds over multiple trips (delivered) via the existing claim/disembark flow. The contract ledger watches settlement and advances delivered; it never moves colonists itself. Cargo is fungible (cargo.contents.colonists is a single integer, no per-pod tagging) — settlement is attributed across a player's open contracts FIFO by created_at.
Fields:
| name | type | constraints | notes |
|---|---|---|---|
| id | UUID | PK | |
| player_id | UUID FK players.id | not null, CASCADE | the brokering player |
| source_planet_id | UUID FK planets.id | not null, CASCADE | the hub planet the contract was brokered at; loading is gated to returning here |
| source_sector_id | Integer | not null | denormalized global sector id, for "return to {sector}" messaging without a join |
| cohort_total | Integer | not null | pioneers brokered in this cohort (1–10,000, MAX_COHORT in pioneer.py) |
| loaded | Integer | not null, default 0 | pioneers currently riding in cargo against this contract (not yet settled) |
| delivered | Integer | not null, default 0 | pioneers settled at frontier worlds |
| fee_per_pioneer_locked | Integer | not null | the 30–80 cr per-pioneer fee (COMMODITY_PRICE_RANGES["colonists"]), snapshotted at broker time — later fee-quote changes don't affect an open contract |
| status | Enum migration_contract_status |
not null, default BROKERED |
BROKERED / IN_PROGRESS / FULFILLED / VOID |
| created_at | DateTime | not null | brokered time; FIFO settlement-attribution ordering key |
| updated_at | DateTime | not null | most recent mutation |
Invariant: delivered + loaded <= cohort_total and delivered <= cohort_total.
States:
- BROKERED — created, fee locked, nothing loaded yet.
- IN_PROGRESS — at least one pioneer loaded into cargo and/or settled.
- FULFILLED — delivered == cohort_total.
- VOID — cancelled by the player (only while loaded == 0), or reabsorbed when the carrying ship was destroyed with nothing yet delivered.
Indexes:
- (player_id, status) — the "list my active contracts" query and the fulfillment match.
- (player_id, source_planet_id) — the load-batch lookup (contract at the hub you're landed on).
Relationships: none declared on the model (player_id / source_planet_id are plain FKs, no ORM relationship()).
Lifecycle¶
- Broker (
POST /pioneer/contracts, landed on a population hub) — creates aBROKEREDcontract with the fee quoted live (pioneer_service.quote_fee, clamped 30–80 cr) and locked onto the row. No credits charged yet. - Load (
POST /pioneer/contracts/{id}/load, landed at the same hub the contract was brokered at) — chargesfee_per_pioneer_locked × quantity, clamped to whichever is tightest of: pioneers remaining on the cohort, ship's free cargo space, and affordable credits (400 with the specific binding reason — never silently truncated). Moves the contract toIN_PROGRESSon first load. - Settle — happens through the existing claim/disembark flow, not a Pioneer Office endpoint.
pioneer_service.attribute_settlementruns whenever colonists settle on a frontier world: it advancesdeliveredon the player's openIN_PROGRESScontracts FIFO bycreated_at, movingmin(remaining, loaded)fromloadedtodeliveredper contract until the settled quantity is exhausted. A contract flips toFULFILLEDthe momentdelivered >= cohort_total. Settled colonists not carried against any contract (e.g. station-bought, or embarked off another owned planet) are simply not attributed to anything. - Cancel (
POST /pioneer/contracts/{id}/cancel) — only whileloaded == 0(you cannot void cryosleep pods physically in your hold; settle or disembark them first). The brokerage fee is not refunded — it was a fee, not escrow. - Reabsorb on ship loss (
pioneer_service.reabsorb_on_ship_loss, best-effort, wrapped so a ledger hiccup never blocks combat resolution) — cryosleep pods die with the hull (canon). Zeroesloadedon the player's open contracts;VOIDs any that have delivered nothing yet.
Routes (api/routes/pioneer.py, mounted under /pioneer):
- GET /pioneer/office — one-round-trip venue payload: live fee quote, ship's colonist cargo + free space, and the player's open contracts.
- POST /pioneer/contracts — broker.
- POST /pioneer/contracts/{id}/load — load a batch.
- GET /pioneer/contracts — list (open by default, include_closed=true for all); reachable from anywhere, not just the hub.
- POST /pioneer/contracts/{id}/cancel — void.
- POST /pioneer/surplus/sell — not part of this ledger — the separate planet-surplus pioneer sale (docked at a Class-0 station, sells a planet's production-tick-accrued surplus; see lifecycle.md §1.4). Included in this router only because it shares the Pioneer Office venue conceptually.
Not covered here: the planet-surplus pioneer sale's own ledger (Planet.active_events['surplus_pioneers']) — that's a separate faucet with no MigrationContract row involved; see ../FEATURES/economy/lifecycle.md.