---
title: "Host Arrivals"
slug: host-arrivals
section: Portals
group: Host Portal
status: stable
description: "One canonical arrival-information record per camp — edited from the Host Portal's existing **Arrival Info** location, shared by every program at that camp, with a required airport transportation question and an explicit applicant review-…"
last-updated: 2026-10-07
---

# Host Arrivals

One canonical arrival-information record per camp — edited from the Host Portal's existing **Arrival Info** location, shared by every program at that camp, with a required airport transportation question and an explicit applicant review-and-confirm step after placement.

> **Who reads this**
>
> Globant platform engineers, Host Portal engineers, Applicant Portal (PPPToo) engineers, and Globus staff console engineers. This page freezes the target contract for the camp-wide ArrivalInformation record (target state, not shipped: development HEAD still serves per-row `POST /api/partner/host/arrivals` and `PATCH`/`DELETE /api/partner/host/arrivals/:id`, which use `editStatus` and a 409 `EDIT_STATUS_CONFLICT` rather than `revision`/If-Match, has no `POST /api/portal/arrival-information/confirm` route, and its only completeness route is the separate host-profile `GET /api/partner/host/profile/completeness`, so the camp-wide Endpoints and Validation sections below describe the target, not current behavior) — identity, transportation question, validation, migration, projections, and the applicant confirmation flow. Implementation lives in the Globus platform repo (BackOfficePrototype), the Host Portal repo (HostPortalPrototype), and the Applicant Portal repo (PPPToo), not in this docs site.

## Purpose & Scope

Hosts publish the airport, transportation arrangements, and first-24-hours instructions a participant uses to plan international travel. Today that data is fragmented: one `host_arrivals` row per (host_org_id, program_id), a parallel copy packed into each Job Order's `requirements.arrivalInstructions`, and an applicant view that lets the Job Order copy win per-field. The confirmed product decision replaces that with **one camp-wide record**, no per-program or per-Job-Order overrides, and a participant confirmation contract so sponsors can prove the participant reviewed the arrangements that actually apply.

## Product Decision (Frozen)

- **Arrival Information stays where it is.** Host users continue to maintain arrivals from the Host Portal's existing **Arrival Info** surface (My Camp → Arrival Information). It does not move to Camp Overview.
- **One record per camp.** A camp has exactly one arrival-information record shared by all of its programs — Camp SWT (support staff) and Camp Counselor alike.
- **Airport transportation is a required question on that record.** It is not a Camp Overview field and not a Job Order answer.
- **No overrides.** There are no program-, offer-, position-, or Job Order-specific transportation values. Job Order and Offer Details surfaces become read-only projections.
- **Participants confirm, never edit.** After a confirmed placement, the Applicant Portal presents the camp-wide arrangements and records an explicit, versioned confirmation.

## Current-State Gap Matrix

Verified against the `development` branches of the three implementation repos (August 2026). Every row below cites the module that makes the claim true today.

| # | Current behavior (shipped) | Source | Gap vs. target |
| --- | --- | --- | --- |
| G1 | `host_arrivals` is keyed per (host_org_id, program_id): the applicant resolver reads the row `where partnerOrgId = hostOrg and programId = jobOrder.programId`. | BackOfficePrototype `server/services/portalArrivalInformation.ts` | Target key is the camp (host org) alone; the program dimension is removed. |
| G2 | The applicant view merges two sources and lets the Job Order's packed `requirements.arrivalInstructions` win per-field (`firstNonEmpty(jobOrderSide.x, hostSide.x)`). | BackOfficePrototype `server/services/portalArrivalInformation.ts` (`mergeArrivalInformation`) | The camp record becomes the only source; Job Order values no longer supersede. |
| G3 | A bidirectional mapping backfills `host_arrivals` from Job Order values and prefills Job Orders from `host_arrivals` (`hostArrivalToJobOrderPrefill`, `jobOrderArrivalToHostArrivalFields`). The Job Order to `host_arrivals` direction copies `bestAirport` into `nearestAirportToJobsite` only when it is a canonical IATA code (`isKnownAirportCode`); a legacy formatted display label is dropped, never guessed. The same applies to `additionalAirport`. | BackOfficePrototype `shared/hostArrivalJobOrderMapping.ts`, `server/routes/jobOrders/mutations.ts`, `client/src/pages/job-orders/use-host-arrival-prefill.ts` | Backfill is retired; the mapping survives only as a one-time migration input. |
| G4 | There is no transportation-mode question anywhere: pickup is inferred (`pickUpProvided = cost !== null` absent an explicit Job Order boolean). | BackOfficePrototype `server/services/portalArrivalInformation.ts` | A required `airport_transportation` enum with conditional details replaces inference. |
| G5 | The Host Portal Arrival Info tab creates one arrival record per programme: Add Arrival Instructions posts `createArrival` with the `programId` of every assigned programme that has no record yet (one record is allowed per programme), and the tab renders a card list ordered by the host's programme order. | HostPortalPrototype `client/src/components/my-camp/arrivalInfo/ArrivalInfo.tsx` | One camp-wide editor; the program selector no longer partitions arrivals. |
| G6 | PPPToo renders a read-only arrival page from `GET /api/v1/arrival-information`, a BFF proxy of the Hub's `GET /api/portal/arrival-information`; flat 200 in both states, no confirmation write exists. | PPPToo `server/src/portal/arrival-information.controller.ts`, `client/src/pages/ArrivalInformationPage.tsx` | Add explicit participant confirmation with revision attribution and staleness. |
| G7 | Offer Details joins a transportation block from `host_arrivals` on (host_org_id, program_id). | BackOfficePrototype `server/routes/hostPortal/offerDetails.ts`; [Host Offer Details](/docs/host-offer-details) | The join key becomes the host org alone; the block stays read-only. |

## Globus System of Record

Globus owns the arrivals data. The camp-wide ArrivalInformation resource is a projection over a single `host_arrivals` row keyed by `host_org_id` alone (unique index on `host_org_id` after migration). The Host Portal is a proxy/cache; the Applicant Portal BFF is stateless. Both write surfaces (host-facing API and Globus-internal staff console) write the same row, run the same validation, emit the same activity event, and feed the same audit trail.

> **One row per camp, two write surfaces, three read surfaces**
>
> Host Portal PATCH and Globus staff PATCH update the same row. Read surfaces are: the Host Portal Arrival Info tab, the Offer Details `participant_transportation` projection, and the applicant-facing arrival-information endpoint. None of them may maintain a parallel store, and none of them may write transportation values.

## Camp-Wide Arrival Record

### Identity & Uniqueness

- The record is identified by the camp's `host_org_id`. Exactly one non-deleted record may exist per host org — enforced by a unique index, not application code.
- The `program_id` column is dropped from the write path. For wire compatibility, host-facing GET still accepts a `program_id` query parameter and returns the same camp record for every program the host is enrolled in.
- Every program at the camp — `camp_swt` and `camp_counselor` — reads the identical record. Per the [Camp host verification policy](/docs/host-governance), the "camp" contract covers `camp_swt` everywhere; the same rule applies here.

### Airport Transportation Question (Required)

The record carries a required single-select `airport_transportation` question: *"How do participants get from the airport to camp?"*

| Code | Label | Meaning |
| --- | --- | --- |
| `camp_pickup` | Camp picks participants up | The camp collects participants at the arrival airport. |
| `arranged_shuttle` | Camp-arranged shuttle / transfer service | A shuttle, bus, or transfer service the camp designates; participants book or board per instructions. |
| `public_transit` | Public transportation with instructions | Participants use public transit following the camp's step-by-step instructions. |
| `participant_arranged` | Participants arrange their own transportation | Participants organize the leg themselves; the camp supplies guidance. |

- Codes are stable snake_case identifiers; labels are localizable display strings. The Host Portal and staff console render labels, never codes.
- The question is **required for record completeness** (see below). A record without an answer is `incomplete` and blocks nothing operationally, but surfaces as incomplete on host, staff, and readiness views, and the applicant confirmation flow will not present an incomplete record.
- The answer applies to both SWT and Camp Counselor participants at the camp. There is no per-program variant of this question.

### Conditional Arrangement Details

Conditional fields are required, forbidden, or optional depending on the selected code. "Forbidden" fields are preserved in storage but omitted from projections while the mode does not select them.

| Field | `camp_pickup` | `arranged_shuttle` | `public_transit` | `participant_arranged` |
| --- | --- | --- | --- | --- |
| `pickup_cost` (numeric ≥ 0; 0 = free) | required | required | omitted | omitted |
| `pickup_contact_name` / `pickup_contact_phone` | required | optional | omitted | omitted |
| `shuttle_provider` (free text) | omitted | required | omitted | omitted |
| `transportation_instructions` (free text, 1–4000 chars) | optional | required | required | required |

Mode-independent fields — always part of the record: `nearest_airport` (IATA, validated against the shared airport registry), `return_to_upon_arrival`, `general_arrival_instructions`, `reporting_to`, and the contact block (`contact_name`, `contact_title`, `contact_email`, `should_contact_before_arrival` with conditional `contact_phone_number`).

### Field Source of Truth

| Field | Type | Required for completeness | Source of truth |
| --- | --- | --- | --- |
| `airport_transportation` | enum (4 codes above) | required | camp record (this page) |
| `nearest_airport` | IATA string | required | camp record |
| conditional details | per mode table above | per mode | camp record |
| `general_arrival_instructions` | text | required | camp record |
| contact block | strings/bool | optional | camp record |
| `additional_airport` (Hub `additionalAirport` / `additionalAirportLabel`) | IATA string or null | optional | Hub arrival record; written by the Host Portal's "Additional/Alternate Airport" control. The Hub rejects an empty string, so clearing sends `null`. Reads fall back to the legacy `portOfEntryAirport` value when `additionalAirport` is empty. The participant-facing `GET /api/portal/arrival-information` returns `additionalAirport` from the job order's packed arrival instructions when that value is non-empty, and only otherwise from this record. |
| `arrival_window_from_date` / `arrival_window_to_date` (Hub `arrivalWindowFromDate` / `arrivalWindowToDate`) — the optional "Latest Arrival Window" | ISO `YYYY-MM-DD` or null | optional | Hub arrival record, written and read as-is by the Host Portal BFF. The Hub enforces the pair rule at the write boundary against the effective record (the stored row merged with the update): both dates or neither, and From on or before To, otherwise 400 with `details.code` `arrival_window_incomplete` or `arrival_window_reversed`. An update that touches neither date leaves a legacy half window (To date only, carried over from the retired single `latestArrivalDate` column, which the migration dropped) alone, and a body that still sends `latestArrivalDate` is rejected with 400 `arrival_window_retired_field`. The Host Portal editor applies the same check client-side; clearing sends `null`. |
| `preferred_arrival_days` / `preferred_arrival_times` | int[] / text | not applicable to camp | no longer collected by the Host Portal arrivals editor and never copied into update submissions; the fields remain in the Hub contract and read shape for non-camp program types only |
| `port_of_entry_airport` | IATA string | not applicable to camp | a separate Hub field the Host Portal no longer writes except to send `null` to clear a value saved before the Additional/Alternate Airport mapping; retained for non-camp program types only |

### Completeness Rules (SWT & Camp Counselor)

A camp arrival record is `complete` when **all** hold; the same rule applies to both programs — there is no program-specific completeness variant:

1. `airport_transportation` is answered with an active code.
2. Every field the selected mode marks *required* is present and valid.
3. `nearest_airport` is a known IATA code.
4. `general_arrival_instructions` is non-empty.

Completeness is computed server-side and returned as a read-only `completeness: { complete: boolean, missing: string[] }` block on every GET, so the Host Portal, staff console, and readiness dashboards render one consistent verdict.

### Revision Behavior & Material Changes

- The record carries a monotonic `revision` integer starting at 1. Every successful PATCH that changes at least one **material field** increments it; non-material edits do not.
- Material fields: `airport_transportation`, all conditional detail fields, `nearest_airport`, `pickup_cost`, `general_arrival_instructions`, `return_to_upon_arrival`.
- Non-material fields: contact block, `reporting_to`.
- The revision is returned on every read (host, staff, applicant) and is the value applicant confirmations bind to (see below). Prior revisions are reconstructible from the audit diff trail; there is no separate revision table.

## Endpoints

### Host-Facing Endpoints

| Method | Path | Description | Auth |
| --- | --- | --- | --- |
| GET | /api/partner/host/arrivals?program_id=:program_id | Return the camp's single ArrivalInformation record. `program_id` remains accepted for wire compatibility and only validates enrollment; every program returns the same record. If no record exists yet, returns 200 with editable fields null, `revision` 0, and `updated_at` = epoch sentinel. | X-Host-Org-Id + X-Host-User-Email |
| PATCH | /api/partner/host/arrivals | Partial update of the camp record. No `program_id`: the record is addressed by the authenticated host org. Returns the full record including recomputed `completeness` and (possibly bumped) `revision`. Requires If-Match precondition (`If-Match: "0"` for first write). | X-Host-Org-Id + X-Host-User-Email + If-Match |

### Globus-Internal Write Surface

Globus staff edit the same fields through the internal host-arrivals staff console, addressed by `host_org_id` alone. Both surfaces share validation, the activity event, revision semantics, and the audit trail. The staff console additionally owns the migration conflict-resolution queue (below).

### Applicant-Facing Endpoints

| Method | Path | Description |
| --- | --- | --- |
| GET | /api/portal/arrival-information (Hub) · GET /api/v1/arrival-information (PPPToo BFF) | Existing read, extended: the response drops the Job Order merge and returns the camp record verbatim plus `airport_transportation`, `arrival_revision`, and `confirmation` state (`none`, `confirmed`, `stale`). Still flat 200 in both states; `{ available: false, reason: "NO_CONFIRMED_PLACEMENT" }` pre-placement. |
| POST | /api/portal/arrival-information/confirm (Hub) · POST /api/v1/arrival-information/confirm (PPPToo BFF) | Record the participant's explicit confirmation of the arrangements. Body: `{ arrival_revision }`. See Confirmation Record & Idempotency. |

## Auth & RBAC

Unchanged model, identical headers to the sibling host portal endpoints — see Host Portal Authentication Modes on the [Portal Overview](/docs/portal-boundaries).

| Surface | Principal | Allowed roles |
| --- | --- | --- |
| Host GET | Host user via Host Portal | host_admin, host_hiring_manager, host_finance, host_readonly |
| Host PATCH | Host user via Host Portal | host_admin, host_hiring_manager (host_readonly → 403) |
| Globus-internal PATCH + conflict resolution | Globus staff session | Sponsor Ops, Partner Ops (per existing host-arrivals RBAC) |
| Applicant GET / POST confirm | Applicant session via the PPPToo BFF: the applicant's Bearer token is forwarded to the Hub as `X-Portal-Access-Token` (same server-controlled context propagation as every other applicant-scoped BFF route); the Hub resolves the applicant, application, and placement ownership server-side from that context | the placed applicant only; confirmation is scoped to the applicant's own application, and identity is never taken from client-supplied identifiers |

Participants can never mutate arrangement fields — the confirm endpoint accepts only `arrival_revision`, and any other key is rejected with 422.

## Validation Rules

Identical validation on both write surfaces. Cross-field failures return 422 with field-level details.

| Rule | Detail |
| --- | --- |
| `airport_transportation` code | Must be one of the four active codes; unknown or inactive → 422. |
| Mode-conditional requirements | Fields marked *required* for the selected mode must be present in the resulting record state (not necessarily the PATCH body); *omitted* fields are accepted but excluded from projections. |
| `pickup_cost` | Numeric string ≥ 0, two decimal places, no currency symbol; 0 means free. |
| `nearest_airport` | Known IATA code per the shared airport registry (same predicate the Job Order mapping used). |
| Concurrency | If-Match on `updated_at`; mismatch → 409 with the current record so the loser rebases. |
| Confirm body | `arrival_revision` must equal the current record revision → otherwise 409 `STALE_REVISION` with the current record so the UI re-presents before re-confirming. |

## Migration & Consolidation

### Deterministic Consolidation Rules

Migration collapses each camp's per-program `host_arrivals` rows and its Job Orders' packed `requirements.arrivalInstructions` into one camp record. Rules run per camp, in order, and are deterministic:

1. **Zero rows, no Job Order values** → no record created; camp starts empty (`revision` 0 sentinel).
2. **Exactly one source row, no conflicting Job Order values** → that row becomes the camp record verbatim; `airport_transportation` is left unanswered (never inferred), so the record is `incomplete` until the host answers it. A recorded `transportPickUpCost` does **not** auto-select `camp_pickup`.
3. **Multiple rows or Job Order values that agree** (field-by-field equal after trim/normalization, per the normalization already codified in `shared/hostArrivalJobOrderMapping.ts`) → merged into one record; agreement is not a conflict.
4. **Any field where sources diverge** → the field is left empty on the camp record and a conflict entry is queued for staff (below). The system never silently selects one divergent value — this explicitly supersedes the shipped `firstNonEmpty(jobOrderSide, hostSide)` precedence.
5. Original per-program rows and Job Order packed values are preserved read-only for audit; nothing is destructively overwritten.

### Staff Conflict Resolution

- Each divergent field produces one entry in a staff-visible **arrival migration conflict queue** (Globus staff console), showing every candidate value with its source (program row or Job Order) and last-modified metadata.
- Staff resolve by picking a candidate or entering a corrected value; each resolution writes the camp record through the standard staff PATCH (same validation, audit, revision semantics).
- A camp with open conflicts reads normally — resolved/unconflicted fields are served; conflicted fields are null and enumerated in `completeness.missing`.

### Job Order & Offer Details Compatibility

- **Offer Details** keeps its `participant_transportation` block as a read-only projection, now joined on `host_org_id` alone — every offer at the camp shows identical values. PATCH continues to reject transportation keys with 422. See [Host Offer Details](/docs/host-offer-details).
- **Job Orders** stop collecting arrival instructions: the admin Job Order form's arrival fields become a read-only projection of the camp record, the packed `requirements.arrivalInstructions` is frozen (retained for audit, never read by the applicant resolver), and both directions of the prefill/backfill mapping are retired from runtime.
- The pre-existing per-participant `POST /api/host/arrivals/:id/confirm` row in the Integration Contract remains a different concern (placement-level arrival reporting) and is unaffected.

## Applicant Review & Confirmation

After a confirmed placement (`placements.status` in `confirmed`/`active`, same eligibility predicate as the shipped resolver), the Applicant Portal presents the camp-wide arrangements and asks the participant to explicitly confirm them. Participants review and confirm only — there is no participant edit surface, and participant flight itineraries or arrival check-ins are out of scope here.

### Confirmation Record & Idempotency

Each confirmation persists in Globus with:

- `applicant_id` and `application_id` attribution (resolved server-side from the placement, never client-supplied),
- `arrival_revision` — the record revision that was on screen,
- `confirmed_at` timestamp and the portal actor.

Idempotency: one live confirmation per (application_id, arrival_revision). Re-posting the same revision returns 200 with the existing confirmation, unchanged (no duplicate rows, no re-emitted event). Posting a non-current revision returns 409 `STALE_REVISION`.

### Stale Confirmation & Reconfirmation

- When a host or staff edit bumps the record revision (material change), every confirmation bound to an older revision becomes **stale**. Stale confirmations are never deleted or mutated — history is append-only.
- The applicant GET then reports `confirmation: "stale"` with both the confirmed and current revisions; the portal re-presents the arrangements and requires reconfirmation.
- Non-material edits do not bump the revision and do not invalidate confirmations.

## Activity & Audit Events

- Every successful host or staff PATCH emits one `host.arrivals.updated` event (actor `host_user` | `staff_user`, changed-fields diff, and now `revision_before` / `revision_after`). The `affected_offer_ids` array enumerates every active offer at the camp, since all of them project the record.
- Each migration conflict resolution emits `host.arrivals.updated` with actor `staff_user` and a `migration_conflict_id` reference.
- Each participant confirmation emits one `applicant.arrival_information.confirmed` event carrying applicant/application attribution and `arrival_revision`. Reconfirmations after staleness emit a new event; the prior event and confirmation row are preserved.
- No double-eventing: the Host Portal and PPPToo BFF are proxies and never re-emit.

## Notifications

Routed through the Notification Model:

- A material change (revision bump) on a camp with confirmed placements → notify assigned partner staff and the affected participants that the arrangements changed and reconfirmation is required (in-app + email).
- `airport_transportation` changing away from `camp_pickup` with placements pending arrival → critical-priority partner notification (pre-arrival expectations changed materially).
- Migration conflicts queued → daily digest to Sponsor Ops until the camp's queue is empty.
- Staff edits carry actor type `staff_user` in the notification subject, as elsewhere.

## Acceptance Criteria

Feature: camp-wide arrival information

- **Host editing** — Given a host enrolled in both `camp_swt` and `camp_counselor`, when a `host_admin` saves Arrival Info, then one record is written and a subsequent GET for either program returns identical values and the same `revision`.
- **All-program consistency** — Given the camp record, when Offer Details or a Job Order view renders transportation, then values equal the camp record field-for-field and every write attempt against them fails with 422.
- **Required question** — Given a record without `airport_transportation`, then `completeness.complete` is false with `airport_transportation` in `missing`, and the applicant confirmation flow does not present the record for confirmation.
- **Migration determinism** — Given divergent legacy values for the same field, when migration runs, then the camp field is empty, a conflict entry exists for staff, and no source value was silently selected.
- **Applicant review/confirmation** — Given a confirmed placement and a complete record at revision N, when the participant confirms, then a confirmation with (application, revision N, timestamp) persists, and re-posting revision N returns the same confirmation without a new row.
- **Stale confirmation** — Given a confirmation at revision N, when a material host edit produces revision N+1, then the applicant GET reports `stale`, confirming revision N returns 409 `STALE_REVISION`, confirming N+1 succeeds, and the revision-N confirmation remains in history.

## Cross-References

- [Host Offer Details](/docs/host-offer-details) — read-only `participant_transportation` projection.
- [Applicant Portal — Endpoint Reference](/docs/applicant-portal-endpoints) — PPPToo BFF arrival-information routes.
- [Portal Overview](/docs/portal-boundaries) — authority boundary: portals present, Globus decides.
- [Job Orders](/docs/job-orders) — retired arrival-instruction collection.

## Implementation Follow-up

This page is a docs-site spec only. Three companion tickets are filed in the house format (docs/asana-ticket-standard.md) on the owning boards, cross-referencing one another by full title:

1. "Globus: camp-wide arrival-information record, migration conflict queue, and applicant confirmation API" — Globus data/API/migration work.
2. "Host Portal: camp-wide Arrival Info editor with required airport transportation question" — Host Portal Arrival Info surface.
3. "Applicant Portal: arrival information review and versioned confirmation flow" — PPPToo BFF + UI confirmation work.

The Host Portal arrivals editor no longer collects the preferred arrival days/times, and it maps the "Additional/Alternate Airport" control to the Hub's `additionalAirport` field rather than `port_of_entry_airport` (sent only as `null` to clear a legacy value); those legacy fields stay contract-retained for non-camp program types and are not shown on camp editors.
