# Supply integration workflows

These flows use the `/api/v1/supply` prefix. The examples under [examples](examples/README.md) can be run only after OutReserve configures an authorized environment and Supply credential; this preview does not assert that a sandbox exists.

## New property onboarding

1. Read the connection and its `capabilities`. Record the source account, permissions, and supported typed unit fields.
2. Walk `GET /locations` through `countries → states → destinations` and select the exact `country_code`, `state_code`, and `destination_slug` returned for the intended location. This read-scoped Supply catalog includes destinations without active consumer properties; the consumer destination index is not a substitute. Create the property with that mapping, a stable external property ID, name, currency, explicit IANA `timezone`, and a fresh `Idempotency-Key`. The time zone is immutable and determines property-local ARI date boundaries; do not infer it from country or destination. Keep the returned OutReserve UUID in the PMS mapping.
   If sending property amenities, read `GET /amenities` first and pass the complete set of writable numeric IDs as `property.amenity_ids`. Omit the field to leave associations unchanged; send `[]` only when intentionally clearing them. See [amenities.md](amenities.md).
3. Create inventory units. Physical units represent specific sites; pooled units represent a quantity of interchangeable accommodation. Preserve each returned UUID. Do not map the same stock through both forms.
4. Add approved public HTTPS images and set their order. Import processing may be asynchronous; inspect each media result before showing a listing as complete.
5. Submit the property for OutReserve review and wait for approval. Phase 1 generic Supply properties remain non-bookable even after content/ARI approval.
6. Set defaults and dated ARI after approval. Verify every intended day has an explicit inventory count. Read back the calendar and resolve gaps, rejected values, and effective closures.

## Associate an existing property

Use `POST /properties/{property_id}/connect` with `{ "connection_request": { "external_property_id": "..." } }` to request an OutReserve-reviewed association. Match the source account and external property ID to the intended OutReserve UUID, confirm current ownership, and wait for approval. Attempts to write an unassociated property return an ownership or permission error. A UUID alone is never proof of control.

```bash
curl --fail-with-body -X POST "$OUTRESERVE_SUPPLY_BASE_URL/properties/$OUTRESERVE_EXISTING_PROPERTY_ID/connect" \
  -H "Authorization: Bearer $OUTRESERVE_SUPPLY_API_KEY" -H 'Content-Type: application/json' \
  -H "Idempotency-Key: $OUTRESERVE_CONNECT_KEY" \
  --data '{"connection_request":{"external_property_id":"pms-property-42"}}'
```

The response exposes only mapping/review metadata until ownership is approved. Do not treat its `201` as a content-write grant. Once approved, read the mapped Units and explicitly send the PMS's default or dated rate for each intended sellable date. An existing OutReserve Unit price is not imported as a Supply source rate; missing partner rates remain unpriced and prevent later reservation activation.

For a legacy-PMS migration, request and review the association before an OutReserve operator uses the Supply Workbench **Legacy PMS cutover** action. The partner cannot perform that cutover through this API. The operator must provide ownership proof, a cutover case, legacy-source shutdown, inventory mapping, and booking-reconciliation references, then type the exact Property UUID. The action takes a current-state lock, refuses future open booking obligations and ambiguous active Unit mappings, atomically stages the Property and every Unit as non-bookable, preserves the former provider/property key, records an actor-linked decision and each Unit's before/after sale gates in the audit ledger, and approves the Supply link with reservations still disabled. A concurrent legacy booking that commits first blocks cutover; cutover that commits first blocks the legacy booking. An operator must reconcile any already-lost provider key before approval. Do not change `pms` through raw SQL or treat the association response as migration authorization. Future bookings or pooled/physical stock requiring remapping need a separate supervised migration; this action does not cancel or transfer bookings, certify ARI, or activate reservations.

## Physical versus pooled inventory

- A physical campsite has `inventory_mode=physical` and capacity of one; each date accepts `available_count: 0` or `1`.
- A pool of six interchangeable cabins has `inventory_mode=pooled` and `capacity: 6`; each date accepts `available_count: 0..6`.
- Stop sell is independent of count. A pool with five remaining cabins can be closed to sales while retaining a count of five.
- An unset date remains unknown/closed. Initial sync and recovery should inspect coverage with ARI readback.

## Daily ARI update

1. Read the current calendar revision and date range.
2. Compose absolute unit counts, rates, and restrictions for the intended dates. Pair each unit count with `incorporated_reservation_sequence`: `0` before any OutReserve reservation events are included, otherwise the highest durably applied sequence for **that property** actually reflected in that count. Sequence values are connection-wide and can skip events for other properties. A property-level update may set a shared rate or stop sell, never inventory count or watermark.
3. Send one bounded atomic ARI request with `If-Match` and `Idempotency-Key`.
4. On `200`, store the new revision and, when needed, read back affected dates.
5. On `stale_revision`, read numeric `context.current_revision` and `context.submitted_revision`, fetch the current calendar, and recompute the desired absolute update; send a new logical request/key. On a timeout, retry the identical request/key first. Do not parse localized `detail` for a revision.

Readback includes each unit/date's reported watermark and `inventory_received_at`; an explicit count write refreshes that timestamp even when the value is unchanged. Rate lookup is unit/date, property/date, unit default, then property default. Unknown inventory remains closed. Before reservation activation, effective sellable count is zero. After activation, it is a daily net count after pending demand and local holds, and can still be zero because of freshness, feed lag, or a closure. It does not evaluate a requested arrival/departure/stay as a quote. An explicit unit closure overrides an open property. Reopening the property changes only the property stop-sell value.

For a property-wide stop sell, send a property-scoped update without `available_count`:

```json
{"updates":[{"scope":"property","date_from":"2026-10-15","date_to":"2026-10-18","stop_sell":true}]}
```

Send it with the same `If-Match` and `Idempotency-Key` discipline as a unit ARI write. A property-scoped rate override uses `rate_minor` in the same shape; it does not create missing unit inventory.

## Media import and audit

Import a public HTTPS image and poll the returned `meta.operation_id` or media UUID. A `202` means the import is queued, not that the image is ready:

```bash
curl --fail-with-body -X POST "$OUTRESERVE_SUPPLY_BASE_URL/properties/$OUTRESERVE_PROPERTY_ID/media" \
  -H "Authorization: Bearer $OUTRESERVE_SUPPLY_API_KEY" -H 'Content-Type: application/json' \
  -H "Idempotency-Key: $OUTRESERVE_MEDIA_KEY" \
  --data '{"media":{"source_url":"https://images.example.invalid/cabin.jpg","alt_text":"Cabin exterior","featured":true}}'
```

Replace the example URL with a publicly fetchable image. `GET /properties/{property_id}/media/{media_id}` reports `processing_status` and an asynchronous `error_code`. `GET /operations/{operation_id}` reports the queued operation status. The request ledger exposes redacted metadata, not original bodies:

Media POST is an idempotent write when retried with the same key, method, path, and raw body. If it times out before you receive the media/operation UUID, retry those exact bytes with the same key; the response returns the original IDs and does not queue another import. A rotating signed URL changes the body and must be a **new** logical operation with a new key; reconcile the existing import before doing that. Review submission and property/unit creation follow the same retry rule. An exact replay returns the original processing snapshot, not necessarily the current asynchronous import status, so poll the resource afterward.

OutReserve retains the import operation before attempting to enqueue work. A five-minute scheduled recovery task re-dispatches at most 100 due imports per tick after a lost queue publication or stale worker claim, with a finite retry budget. Polling may eventually show `media_enqueue_failed` if it cannot be queued or completed; report its operation UUID to OutReserve rather than repeatedly creating new media records. The scheduler and worker must be active in the target environment; a deployed web process alone does not activate this recovery path.

```bash
curl --fail-with-body -H "Authorization: Bearer $OUTRESERVE_SUPPLY_API_KEY" \
  "$OUTRESERVE_SUPPLY_BASE_URL/requests?page=1"
```

Each entry includes request ID, outcome, timing, attempted idempotency key, replay linkage, and changed field names. `GET /requests/{request_id}` fetches one connection-owned entry; do not expect raw secrets, image contents, or field values.

This endpoint covers requests authenticated and handled by the Supply controller. A request rejected by edge protection or Rack rate limiting never reaches that controller, and an invalid credential cannot be attributed to a connection. Its `X-Request-Id` remains useful when contacting OutReserve support, but `GET /requests/{request_id}` may return `404` for such an early rejection; it is not evidence that the request was accepted or applied.

## Deactivation and recovery

Patch the unit or property status to the supported inactive state. Do not send a fictitious zero-inventory history as a substitute for deactivation. Preserve mappings for audit and future reactivation. If a batch fails validation, no dates change; correct the returned JSON pointers and retry as a new logical request. Workbench operators must reconcile against the latest revision before replaying old changes.

```bash
curl --fail-with-body -X PATCH "$OUTRESERVE_SUPPLY_BASE_URL/properties/$OUTRESERVE_PROPERTY_ID/units/$OUTRESERVE_UNIT_ID" \
  -H "Authorization: Bearer $OUTRESERVE_SUPPLY_API_KEY" -H 'Content-Type: application/json' \
  --data '{"unit":{"status":"inactive"}}'
```

Partners cannot reopen a deactivated property or unit by PATCH. For a previously active reservation outlet, a platform administrator can review and reactivate an inactive property or a previously published inactive unit in Supply Workbench. The operator must explicitly confirm reopening sales and provide bounded case references for mapping, delivery, pricing, privacy, reconciliation, and the certification decision. The action checks the current account and property under lock, complete fresh count/rate/watermark coverage across the configured sellable horizon, and an applied reservation feed. Failure leaves the record inactive; a successful action records the actor and redacted proof in the audit timeline. An inactive property must be reactivated before any inactive unit under it. A staged unit uses the separate reviewed publication action. These recovery actions remain behind the default-off reservation activation gate; they do not make an unapproved property bookable.

Media uses a different terminal soft-delete lifecycle. `PATCH /media/{id}` with `status: deleted` removes the image from the collection but retains its UUID, `GET` record, operation, and audits. `status: active` is rejected with `422 invalid_status_transition` at `/media/status`. To replace an image, submit a **new** `POST /media` with the current HTTPS source URL and metadata, then poll its new operation until it succeeds. Do not assume the deleted blob or original URL is still usable, and do not reuse the old media ID. If the deleted image was still importing, its original operation resolves as `media_deactivated`; the replacement has its own operation and request IDs. A failed replacement does not resurrect the tombstone. Review or remove the old reference in the PMS only after storing the new ID; an uncertain POST must be retried with its original idempotency key and body before creating another replacement.

```bash
curl --fail-with-body -X POST "$OUTRESERVE_SUPPLY_BASE_URL/properties/$OUTRESERVE_PROPERTY_ID/media" \
  -H "Authorization: Bearer $OUTRESERVE_SUPPLY_API_KEY" -H 'Content-Type: application/json' \
  -H "Idempotency-Key: $OUTRESERVE_REPLACEMENT_MEDIA_KEY" \
  --data '{"media":{"source_url":"https://images.example.invalid/cabin-replacement.jpg","alt_text":"Cabin exterior"}}'
```

## Reservation loop preview and release boundary

The branch-local implementation has a durable reservation-revision pull feed, acknowledgement, and unit/date inventory-incorporation marker. It is deliberately absent from the Phase 1 OpenAPI/Postman contract and is not a certified live-booking outlet. An acknowledgement means a reservation revision was durably applied by the PMS; it does **not** prove that a later inventory snapshot includes that reservation. See the [reservation preview and remaining gates](reservations.md) and [protocol examples](examples/README.md#reservation-delivery-preview). Do not import third-party guest records as OutReserve bookings simply to block inventory; send the resulting net inventory count instead.
