Skip to main content

BF-007 — OTA Channel Sync (ARI push + booking pull)

Business flow (cross-EPIC user journey). Reconciled 2026-05-25 as Wave 2 of the RTM Scale-Out, per the PLAYBOOK.md template established by BF-001.

Two directions, one flow doc:

  • Outbound — operator edits a rate / inventory / restriction cell in the HMP ARI Grid; the change fans out to channel-service which transforms per-OTA and pushes to ResAvenue (XML) or a direct OTA API.
  • Inbound — an OTA (Booking.com via ResAvenue, MakeMyTrip, Agoda, Expedia, Goibibo) POSTs a new / modified / cancelled booking to the ResAvenue webhook; channel-service normalises to a canonical OtaBookingReceivedEvent / OtaBookingCancelledEvent, and reservation-service applies it.

Per BF-001 step shape: Story → Code → Test → Pre/Post → Notes. Where the user-story catalog has no row for a behaviour that ships in code, the step is tagged [SME-PROPOSED] as a NEW story to surface the gap.

Drift IDs in this doc use the BF-007 D-NNN global scheme — see PLAYBOOK §"What to change", proposed global namespace for cross-EPIC drift.

Personas​

PersonaRoleSource
Revenue Manager / RMSets rates + allocation per OTA on the ARI Grid01-epic-catalog.md EP-009 + EP-011 (RM)
Front Office ManagerReviews OTA bookings after they land01-epic-catalog.md EP-003
Channel OperationsOnboards new OTAs, monitors sync errors, replays webhooks01-epic-catalog.md EP-009
OTA partner system (Booking.com, MMT, Agoda, Expedia, Goibibo, GoMMT)External system pushing inbound bookings + receiving outbound ARIinbound: ResAvenue webhook; outbound: ResAvenue / direct OTA APIs
Guest (indirect)Books on OTA, never touches HMP UI; OTA owns guest comms01-epic-catalog.md EP-004

Entry points​

EntryURL / EndpointBacked by
ARI Grid (outbound trigger)https://dashboard.<env>.keynova.ai/distribution/arihmp-web-dashboard/src/components/modules/ari/ari-grid.tsx + 18 sibling files
Channel Connections adminhttps://dashboard.<env>.keynova.ai/settings/channelshmp-web-dashboard/src/app/(dashboard)/settings/channels/page.tsx
Channel Mappings adminhttps://dashboard.<env>.keynova.ai/settings/channels/mappingshmp-web-dashboard/src/app/(dashboard)/settings/channels/mappings/page.tsx
Sync Logs / AuditGET /api/channel-sync/logs?propertyId=&limit=hmp-channel-service/src/sync/sync.controller.ts
Webhook receiver (inbound)POST /api/webhooks/resavenue/{booking,reservation,cancellation} + /push (HMAC)hmp-channel-service/src/intermediary/providers/resavenue/resavenue-webhook.controller.ts
Webhook replay (recovery)POST /api/webhook-management/events/:id/replay (SUPER_ADMIN)hmp-channel-service/src/webhooks/webhook-management.controller.ts

The 12-step journey​

Step 1: Revenue Manager edits a rate / inventory cell in ARI Grid​

  • Story: US-009-004 — Availability Push to OTAs (Real-Time) + US-009-006 — Rate Push to OTAs. UI surface — [SME-PROPOSED] follow-up US-011-NEW-ARI-GRID "ARI Grid cell edit + auto-publish + per-channel override + DOW pattern + closure dialog" because the AP-T11-011 work (ari-grid.tsx + 18 sibling components: cell, channel-override-dialog, closure-dialog, dow-pattern-dialog, sync-status-strip, history-drawer) is not catalogued anywhere.
  • Code: hmp-web-dashboard/src/components/modules/ari/ari-grid.tsx (cell editor with optimistic mutation + sync-status-strip). PATCH/POST endpoints land via hmp-property-service/src/distribution/distribution.controller.ts → distribution.service.ts::upsertCells() (owner of ARI cell writes). Per-channel override dialog persists to ChannelOverride cell-state metadata.
  • Test: hmp-property-service/src/distribution/__tests__/distribution.service.spec.ts (ARI cell write + fanout), aggregator.allocation.spec.ts (AP-T11-013 cell aggregation), cell-writer.service.spec.ts (cell write idempotency). E2E: hmp-web-dashboard/e2e/ari-grid.spec.ts + e2e/distribution-redirects.spec.ts + e2e/tests/channel-push-ops.spec.ts. Flow: ari-grid.flow.spec.ts :: "ARI grid push lifecycle" — TBD per 2026-05-16-13-flow-demo-readiness.md.
  • Pre-conditions: Operator authenticated as MANAGER / REVENUE_MGR / ADMIN. At least one active ChannelConnection exists for the property. Rate plan + room type mapped to the channel (US-009-002 / US-009-003). Allocation policy (POOL / RATIO / MANUAL per AP-T11-013) configured.
  • Post-conditions: Cell write committed to AriCellState (property-service). AriCellState.allocations reflects per-channel cap. UI optimistically reflects the new value; sync-status-strip shows pending → success/failure as the fanout completes.
  • Notes: The ARI Grid is the only place a RM can edit a per-OTA override without dropping into channel-service admin. The dashboard has both "publish-on-save" (auto-publish) and "stage + flush" modes — operator preference per property.

Step 2: Cell mutation persists via auto-publish or manual flush​

  • Story: US-009-004 + US-009-006 + US-009-007 (Restriction Push — Min Stay, CTA, CTD). Auto-publish semantics — [SME-PROPOSED] follow-up US-009-NEW-PUBLISH-MODES "Per-property cell publish mode (auto vs staged) + debounce window" because the dual-mode pattern is shipped but not catalogued.
  • Code: hmp-property-service/src/distribution/distribution.service.ts::upsertCells() writes the cell then enqueues outbound: in auto-publish mode it immediately calls ChannelServiceClient.distributionFanout(propertyId, [{date, roomTypeId, channelCode?, value, kind: AVAIL|RATE|RESTRICTION}]); in staged mode it batches into PendingChannelPush and flushes on operator "Publish Now" or the cron timer.
  • Test: hmp-property-service/src/distribution/__tests__/distribution.service.spec.ts :: auto-publish on cell write + staged flush — PASS. Idempotency assertion in cell-writer.service.spec.ts.
  • Pre-conditions: Step 1 completed. Cell row written to AriCellState in same transaction.
  • Post-conditions: For auto-publish: ChannelServiceClient.distributionFanout() invoked; on success, a ChannelSyncLog row appears with status=PENDING. For staged: a PendingChannelPush row is enqueued.
  • Notes: Debounce + coalescing window (collapse 5 cell edits in 30s into 1 push) is NOT in the catalog story — flagged as drift D-007 in EP-009. Without it, a power-user keyboard-edit session can produce 100+ pushes per minute (and exhaust ResAvenue rate limit).

Step 3: Outbound fan-out — property-service → channel-service /internal/channel/distribution-fanout​

  • Story: US-009-004 + US-009-006. Internal-service contract surface — [SME-PROPOSED] follow-up US-009-NEW-FANOUT-CONTRACT "Internal distribution-fanout contract (AP-T11-011)" because the receiver endpoint + payload shape are AP-flagged but not catalogued.
  • Code: hmp-channel-service/src/internal/internal.controller.ts (and /internal/channel/distribution-fanout handler — AP-T11-011 distribution-fanout receiver). Auth: internal service-account JWT via BaseServiceClient (@hmp/service-clients); JwtAuthGuard fast-path. Tenant context: x-tenant-id / x-property-id headers (NEVER from client body). ParallelSyncService fans out across all matching active connections; AllocationService.resolveCap(propertyId, roomTypeId, date, channelCode) clamps the per-channel push value before adapter dispatch.
  • Test: hmp-channel-service/src/distribution/__tests__/allocation.service.spec.ts (AP-T11-013 POOL/RATIO/MANUAL clamp). parallel-sync.service.spec.ts [CITED-BUT-NOT-FOUND] — verify Task 6.
  • Pre-conditions: Step 2 completed; internal JWT signed by current platform RS256 keypair; receiver assertPropertyTenant(propertyId, tenantId) passes.
  • Post-conditions: One ChannelSyncLog row per (channelId × push) with status=IN_PROGRESS and idempotencyKey (64-char SHA-256 hex per AP-T11-001). Demo-tenant short-circuit may have rejected with DEMO_TENANT_OTA_BLOCKED (Plan 4, 2026-05-13).
  • Notes: assertPropertyTenant is non-negotiable per hmp-docs/docs/architecture/cross-service-communication.md. EP-009 D-001 flags that the createConnection (Step 0/setup) leg of this same controller surface lacks the check — a defense gap, but separate from the per-push receiver guard.

Step 4: Per-channel transformation (ChannelRateMapping.derivedFormula)​

  • Story: US-009-003 — Rate Plan Mapping (derived formula engine: parent + 5%, parent + 350 INR breakfast, etc.). GST resolution mode — [SME-PROPOSED] per EP-009 D-006: ChannelRateMapping.pricePushMode ENUM('GROSS','NET') per OTA expectation (Booking.com=GROSS, Expedia=NET, MMT=GROSS).
  • Code: hmp-channel-service/src/rate-mapping/ — formula engine resolves ChannelRateMapping.formula over the pushed parentRate to produce the per-channel derivedRate. hmp-channel-service/src/connections/connections.service.ts::hydrateRoomMapping/hydrateRateMapping (lines 38-45 / 51-58) — service-client lookup against property-service RoomType/RatePlan (replaces deleted Prisma relation).
  • Test: hmp-channel-service/src/rate-mapping/*.spec.ts [CITED-BUT-NOT-FOUND] per EP-009 RTM. hmp-channel-service/src/connections/connections.service.spec.ts :: createRoomMapping happy path (only 1 test — coverage gap surfaced in EP-009 US-009-002).
  • Pre-conditions: Step 3 completed. ChannelRoomMapping + ChannelRateMapping rows exist for (channel × pmsRoomTypeId × pmsRatePlanId). Formula validates (no divide-by-zero, no recursive parent references).
  • Post-conditions: Per-OTA payload built: {otaRoomTypeCode, otaRateCode, derivedRate, gstAdjustedRate?, currency, dateRange[]}. For ResAvenue: collapsed-date-range XML per AP-T11-007. For direct OTAs: per-OTA JSON.
  • Notes: STD-CP-as-child-rate gotcha (2026-05-05) — ResAvenue PMS Test Hotel has STD-CP set as a child rate of STD-EP; pushing CP returns Invalid xml request. Child rate updates are not allowed. Please try to update Parent rate(s) only. EP-009 D-005 proposes a ChannelRateMapping.partnerSideKind column to skip pushes for CHILD_DERIVED rows. OTA-code format validation (EP-009 D-003) — operator types MMT-DLX-O1 (letter O) vs MMT-DLX-01 (digit zero) and the push silently lands at no-OTA-room. No regex guard today.

Step 5: Push to ResAvenue (XML) / direct OTA APIs​

  • Story: US-009-004 + US-009-006. Multi-currency — [SME-PROPOSED] per EP-009 SME notes: today all push is INR; Booking.com expects per-OTA currency.
  • Code: hmp-channel-service/src/sync/sync.service.ts::pushAvailability(channelId, items) / pushRates() / pushRestrictions() — 3-attempt exponential backoff, idempotencyKey persistence (AP-T11-001 2026-05-08 — 64-char SHA-256 keyed on propertyId|channelId|kind|sortedItems), per-payload SHA-256 dedup. hmp-channel-service/src/intermediary/providers/resavenue/resavenue.adapter.ts::pushRates/pushAvailability populates SyncResult.requestXml / responseXml. hmp-channel-service/src/intermediary/providers/resavenue/resavenue-xml.builder.ts — Spec v1.7 alignment: <OTAName> omit-default (AP-T11-007), <Promotion> extension (AP-T11-014), single-pax BaseByGuestAmt divergence (AP-T11-015), date-range collapse (2026-05-05), MinStay/MaxStay inside <Rate>. RESAVENUE base URL + cert: RESAVENUE_BASE_URL, RESAVENUE_CERT_PATH (v2 today; v3 cutover tracked as FU-6).
  • Test: hmp-channel-service/src/sync/sync.service.spec.ts :: 14 tests incl. AP-T11-001 idempotencyKey 64-char hex + retry-stability + value-change-changes-key — PASS. resavenue-xml.builder.spec.ts :: 71 specs covering <Promotion>, single-pax, date-range collapse, <OTAName> omit/emit — PASS. resavenue.adapter.spec.ts — adapter push/pull. Flow: resavenue-cert-cycle.flow.spec.ts :: "RA cert cycle end-to-end" + channel-sync.flow.spec.ts :: "push-availability lifecycle" + channel-manager-multi.flow.spec.ts :: "multi-channel fan-out" — TBD per 13-flow doc.
  • Pre-conditions: Step 4 transformation complete. ResAvenue cert v2 reachable from outbound IP. Connection in LIVE mode (not SIMULATOR / not DEMO_TENANT_BLOCKED).
  • Post-conditions: ChannelSyncLog.status flips to SUCCESS / FAILED / PARTIAL. requestXml + responseXml columns populated (2026-05-05 schema). On 4xx/5xx, retry envelope published to channel.sync.retry for SyncRetryConsumer.
  • Notes: <OTAName> master-vs-channel-wise distinction (gotcha, 2026-05-05): outbound XML OMITS <OTAName> by default for master-update (works for ResAvenue); only emits per-channel when credentials.emitOtaName=true. ResAvenue cert FU-6 (v3 re-issuance against EKS ALB) in flight. GST + TDS reverse-charge note: outbound rate push is GROSS-or-NET per channel; the GST adjustment is a transformation concern (Step 4 D-006), not a sync concern.

Step 6: Push response logged (sync logs, audit page)​

  • Story: US-009-012 — Sync Error Monitoring & Retry Queue (Verified-Built per EP-009 RTM — the strongest story in EP-009). US-009-010 — Channel Performance Report.
  • Code: hmp-channel-service/src/sync/sync.service.ts (populates ChannelSyncLog with idempotencyKey, status, error, requestXml/responseXml, retry count). hmp-channel-service/src/sync/sync-retry.consumer.ts — retry-queue consumer with exponential backoff cap. hmp-channel-service/src/stats/* — Stats module (summary, timeline, throughput, alerts). hmp-channel-service/src/webhooks/webhook-management.service.ts — replay endpoint. UI: hmp-web-dashboard/src/components/modules/ari/sync-status-strip.tsx + sync logs page.
  • Test: sync.service.spec.ts (14 tests incl. idempotency + retry-stability) — PASS. intermediary-audit-anomaly-detector.spec.ts (5 anomaly conditions: WHITELIST_BECAME_EMPTY, LARGE_REMOVAL, OFF_HOURS, RAPID_EDITS, BULK_PROVIDER_EDITS) — PASS. intermediary-config-notifier.spec.ts (Slack notifier) — PASS. webhook-management-replay.spec.ts — PASS.
  • Pre-conditions: Step 5 completed (success OR failure both produce a log row).
  • Post-conditions: ChannelSyncLog row with terminal status. On persistent failure (3 retries exhausted), an alert publishes to notification.sync.failed for ops paging. Stats module aggregates throughput + error rate for dashboards.
  • Notes: AP-T11-024 Phase 4B+4C anomaly detector watches the audit log for credential-edit anomalies (whitelist suddenly empty, after-hours edits, etc.) — surfaces malicious-insider patterns. Slack notifier wires both audit and anomaly webhook URLs.

Step 7: (Inbound) OTA booking received via webhook​

  • Story: US-009-005 — Booking Pull from OTAs (catalog Partial; EP-009 D-009 surfaces gaps in cert-lifecycle + IP-allowlist-modes acceptance criteria).
  • Code: hmp-channel-service/src/intermediary/providers/resavenue/resavenue-webhook.controller.ts — POST /api/webhooks/resavenue/booking|reservation|cancellation (XML + JSON parser paths) + POST /api/webhooks/resavenue/push (HMAC variant per AP-T11-007). hmp-channel-service/src/intermediary/abstract-intermediary-webhook.controller.ts — Base controller: persists raw bytes to WebhookEvent (2026-05-05 schema columns), extracts dual-auth credentials (Classic / UserName+Password per AP-T11-007), enforces IP allowlist (ENFORCE / SHADOW / OFF per AP-T11-024 Phase 4E).
  • Test: resavenue-webhook-booking.spec.ts (booking webhook ingestion), resavenue-webhook-hmac.spec.ts (HMAC /push signature), resavenue-xml.parser.spec.ts (multi-RoomStay coverage), resavenue-cert-format.spec.ts (15 cert-format gates), abstract-intermediary-webhook.spec.ts (14 specs for dual-auth + IP allowlist branches). Flow: resavenue-cert-cycle.flow.spec.ts :: "push → modify → cancel" with X-Forwarded-For=203.109.97.241 (cert sandbox IP).
  • Pre-conditions: ResAvenue connection seeded with whitelistedIps=[203.109.97.241, 122.182.6.210] (both required post-2026-05-05 — the 2nd was missing pre-2026-05-05 and surfaced as REJECTED_IP after Phase 4E ENFORCE default). Connection in LIVE mode. Webhook URL registered with ResAvenue via webhook-registration.service.ts::registerWebhookWithResAvenue (auto-fires when mode flips LIVE).
  • Post-conditions: WebhookEvent.rawPayload persisted (whether accepted OR rejected). On accept: handed to normalizer. On reject (IP/HMAC/cert): 403 + audit row; SHADOW mode logs + accepts + audits.
  • Notes: ResAvenue is the only OTA today with a real bidirectional webhook path. Booking.com / MMT / Agoda / Expedia / Goibibo all flow THROUGH ResAvenue (routedVia: RESAVENUE). Webhook events from those OTAs land on the same controller with partnerCode field discriminating downstream.

Step 8: HMAC signature validated (where applicable) + IP allowlist enforced​

  • Story: US-009-005 + US-009-001 (Channel Connection — credentials carry HMAC secret). HMAC + IP-allowlist modes — [SME-PROPOSED] per EP-009 D-009 already in flight.
  • Code: hmp-channel-service/src/intermediary/providers/resavenue/resavenue-webhook.controller.ts::validateHmac() uses crypto.timingSafeEqual against IntermediaryConnection.credentials.webhookSecret. IP allowlist three-mode resolver: IntermediaryConnection.enforcementModeOverride wins over IntermediaryProviderConfig.enforcementMode; default ENFORCE.
  • Test: resavenue-webhook-hmac.spec.ts — covers timing-safe-equal, wrong secret, missing header, base64 vs hex. abstract-intermediary-webhook.spec.ts — 14 specs covering ENFORCE/SHADOW/OFF × allowed/denied × headerless paths.
  • Pre-conditions: Step 7 received raw bytes; HMAC header present (for /push) OR IP in allowlist (for /booking|reservation|cancellation).
  • Post-conditions: Verified request continues to Step 9. Rejected request: 403, audit row, optional Slack notification for repeated denials.
  • Notes: SHADOW mode is the unsung hero — lets operators preview enforcement impact before flipping. EP-009 D-009 proposes catalog this. Trust-boundary note (resavenue-cert-cycle.flow.spec.ts comment): X-Forwarded-For is only honored because the gateway is the trusted edge in cert env; in prod the gateway STRIPS client-supplied X-Forwarded-For.

Step 9: Inbound mapping — OTA roomCode → HMP roomTypeId via IntermediaryMapping​

  • Story: US-009-002 (Room Type Mapping) + US-009-003 (Rate Plan Mapping). EP-009 D-002 flags missing role guards + missing FK validation on the mapping CRUD.
  • Code: hmp-channel-service/src/intermediary/channel-booking-normalizer.service.ts::normalize{Reservation,Cancellation,Modification} — reads IntermediaryMapping rows for (connectionId × otaRoomTypeCode) to resolve pmsRoomTypeId + (otaRateCode) to pmsRatePlanId. Produces canonical OtaBookingReceivedEvent / OtaBookingCancelledEvent per @hmp/api-contracts@1.9.4 (schemaVersion 1.0).
  • Test: channel-booking-normalizer.spec.ts — canonical event shape, multi-RoomStay (gotcha 23, 2026-05-05), cancellation, modification. intermediary/__tests__/connection-cross-tenant-denial.spec.ts — 11 tests including "encryption.decrypt is never called for foreign-tenant rows" (Task #2 deliverable; status: orphaned — not in CI gate per EP-009 D-001).
  • Pre-conditions: Step 8 succeeded. IntermediaryMapping rows seeded for connection × roomCode (ResAvenue PMS Test Hotel: 3 room types).
  • Post-conditions: Canonical event published to channel.booking.received / channel.booking.cancelled topic with pmsRoomTypeId, pmsRatePlanId, tenantId, propertyId, otaBookingId, partnerCode, dates, pax, rate, GST split, guest profile.
  • Notes: Missing mapping → normalizer raises OTA_MAPPING_MISSING and publishes a channel.mapping.broken event for ops alerting (EP-009 D-004 proposes formalising this with a daily integrity-check cron). Today this is best-effort; bookings can land in a void.

Step 10: Reservation created in reservation-service (status=CONFIRMED, source=OTA)​

  • Story: US-003-001 — Create Individual Reservation (single + multi-room via roomReservations[]) + US-003-013 — Cancel Reservation (for the cancellation arm). OTA-source-specific behaviours — [SME-PROPOSED] per EP-003 SME notes "OTA multi-RoomStay ingestion path added event.rooms[] consumer support"; story body silent.
  • Code: hmp-reservation-service/src/consumers/channel-booking-received.consumer.ts (subscriberId channel-booking-received-consumer) — calls ReservationsService.createFromOtaEvent() with idempotency on otaBookingId. AP-T03-004 OTA tax-inclusive reverse-derivation (isTaxInclusive=true + expectedTotalAmount reconciliation guard). ChannelBookingReceivedConsumer.applyModification handles notificationType: 'MODIFICATION' events (date/pax/specialRequests; per-room rate/room-type out of scope for v1; terminal-state reservations log WARN + skip — EP-009 D-012 proposes manual-reconciliation-queue replacement). hmp-reservation-service/src/consumers/channel-booking-cancelled.consumer.ts (subscriberId channel-booking-cancelled-consumer) calls ReservationsService.cancel() with reason="OTA cancellation via <partnerCode>".
  • Test: reservations.sprint2.spec.ts :: 13 BookingSource enum coverage + sourceReference + Kafka event payload assertions. reservations.service.spec.ts :: AP-T03-004 OTA tax-inclusive specs. channel-booking-received.consumer.spec.ts [CITED-BUT-NOT-FOUND] (verify Task 6). Flow: resavenue-cert-cycle.flow.spec.ts :: "push → modify → cancel" end-to-end.
  • Pre-conditions: Step 9 published canonical event. Reservation-service subscribed to topic (event-dispatcher consumer-group not skipped — SKIP_EVENT_SUBSCRIPTIONS=false).
  • Post-conditions: Reservation row with status=CONFIRMED, source ∈ {BOOKING_COM, EXPEDIA, MAKEMYTRIP, AGODA, GOIBIBO, AIRBNB, GDS}, sourceReference=otaBookingId, bookingChannel=RESAVENUE (or direct), tax-inclusive total reconciled. RoomReservation rows (1 or N for multi-RoomStay). ReservationHistory row with performedBy='channel-service:<partnerCode>' (per EP-003 SME proposal, currently performedBy=null for OTA-driven mods — D-driftable).
  • Notes: Idempotency on otaBookingId prevents duplicate creation when ResAvenue replays a webhook (which happens during cert cycles). EP-003 SME flagged P0 risk: OTA cancellation cancellationCharge from the event is currently ignored; local policy applies on top → double-charge risk for OTAs that already debited via VCC (EP-003 D-007).

Step 11: Guest created (or matched) — GUEST_CREATED / matched event​

  • Story: US-004-001 (Guest CRUD — Create / Search / Match). Inline-guest from OTA — [SME-PROPOSED] follow-up US-004-NEW-OTA-INLINE-MATCH "OTA-sourced guest match-or-create with phone+email fuzzy match" because OTA bookings come with minimal guest profile (name, email, phone, country) and the merge-vs-create heuristic is not catalogued.
  • Code: hmp-reservation-service/src/reservations/reservations.service.ts::create() orchestrates inline-guest creation via GuestServiceClient.matchOrCreate(payload). Match heuristic: email exact match (+ tenant scope) → reuse; else phone match → reuse; else create new Guest row + emit GUEST_CREATED event (consumed by CRM service for welcome flow, loyalty tier eval, etc.).
  • Test: hmp-guest-service/src/guests/guests.service.spec.ts :: match-or-create branches. EP-003 RTM cites OTA-side spec in reservations.service.spec.ts (multi-room + OTA-source coverage).
  • Pre-conditions: Step 10 in flight; canonical event carries guest: {firstName, lastName, email?, phone?, country?, ...}.
  • Post-conditions: Guest row exists (matched or created). Reservation.guestId populated. GUEST_CREATED published on first creation only (idempotent on email+phone+tenant). DPDPA / GDPR consent flag set conservatively (no marketing opt-in; OTA owns consent capture).
  • Notes: OTA-supplied phone is often the OTA's masked relay number (e.g. +91-NN-NNNN-XXXX for Booking.com Phone Privacy). Storing this as Guest.phone and treating it as a real number breaks SMS dispatch. Drift candidate: store under Guest.relayPhone separately; expose unmask via OTA API per booking when available.

Step 12: Confirmation flow — no email from HMP (OTA owns guest communication)​

  • Story: [SME-PROPOSED] new story US-009-NEW-OTA-COMMS-BOUNDARY "HMP suppresses guest-facing confirmation/cancellation comms for OTA-sourced reservations; ops-internal notifications only" because the policy (don't email the OTA guest) is shipped behaviour but not catalogued, and the boundary is critical to brand parity + spam-trap compliance.
  • Code: hmp-notification-service/src/consumers/reservation-confirmed.consumer.ts — gated on reservation.source NOT IN (BOOKING_COM, EXPEDIA, MAKEMYTRIP, AGODA, GOIBIBO, AIRBNB, GDS). For OTA-sourced rows: emits an internal ops.reservation.received notification (front-office Slack / dashboard toast) instead of a guest email.
  • Test: reservation-confirmed.consumer.spec.ts [CITED-BUT-NOT-FOUND] — verify source-discriminator branching. hmp-notification-service/src/__tests__/ota-suppression.spec.ts [CITED-BUT-NOT-FOUND].
  • Pre-conditions: Step 10 emitted reservation.confirmed. Notification-service consumer running.
  • Post-conditions: NO guest-facing email/SMS sent. Internal ops.reservation.received notification fires to property's front-office channel. NotificationLog row with suppressed: true, suppressionReason: "OTA_BOOKING_PARTNER_OWNS_COMMS".
  • Notes: Booking.com explicitly forbids hotels from spamming guests via Booking.com-relayed addresses; violations risk delisting. Same for MMT / Agoda. Pre-arrival upsell (3 days before check-in) MAY use the relay channel via OTA's messaging API (Booking.com Messages API, Expedia Guest Messaging) — that's a separate Day-2 follow-up, NOT part of the confirmation flow.

Failure modes worth testing​

#ScenarioExpected behaviourCoverage todayOwning story
F1Stale rate push (operator edits then reverts within 30s; pushes fire 2x)Debounce / coalesce window collapses to 1 push; idempotencyKey de-dups at adapterPartial — sync.service.spec.ts covers idempotencyKey de-dup; no debounce testUS-009-004 (EP-009 D-007)
F2Webhook IP rejected by allowlist (ENFORCE mode)403 + WebhookEvent.rawPayload persisted with status=REJECTED_IP + audit row + Slack alert on repeated denialsabstract-intermediary-webhook.spec.ts (14 specs) — PASSUS-009-005 (EP-009 D-009)
F3HMAC mismatch on /push403 + audit; timing-safe-equal prevents timing oraclesresavenue-webhook-hmac.spec.ts — PASSUS-009-005
F4Inbound mapping missing (OTA roomCode not in IntermediaryMapping)OTA_MAPPING_MISSING raised; channel.mapping.broken published; reservation NOT created; ops alertedPartial — normalizer raises but no daily integrity cron; mappings can rot silentlyUS-009-002 (EP-009 D-004)
F5Overbooking from race (2 OTAs pull last-room simultaneously, both webhook in within 100ms)First booking creates; second hits ROOM_TYPE_OVERSOLD safeguard; reservation-service marks status=OVERSOLD, emits ops.oversold.detected for manual reassignPartial — reservations.service.spec.ts covers oversold detection; race-condition spec NOT in test suiteUS-003-001 + US-009-005
F6OTA outage (ResAvenue 5xx on push for 30 min)3-retry exponential backoff per push; persistent failure publishes notification.sync.failed; ChannelSyncLog.status=FAILED; sync-status-strip flashes red; circuit breaker opens after thresholdPartial — sync.service.spec.ts covers single-attempt retry; circuit-breaker not yet wiredUS-009-004 + US-009-012
F7Partial push success (3 of 5 channels succeed, 2 fail)Each channel logs independently; aggregate status PARTIAL; failed channels enter retry queue; succeeded channels NOT retried (idempotencyKey blocks duplicate)sync.service.spec.ts retry-stability — PASSUS-009-004 + US-009-012
F8Cross-tenant push collision (malicious tenant supplies victim's propertyId in ChannelConnection.propertyId)403 + CROSS_TENANT_PROPERTY_DENIED before any DB writeOrphan spec only — connection-cross-tenant-denial.spec.ts covers READ-side; CREATE-side UNGUARDEDUS-009-001 (EP-009 D-001 — P0)
F9Demo / sandbox tenant accidentally pushes to real OTA400 DEMO_TENANT_OTA_BLOCKED + WARN log; auth-service outage fail-safe = treat as NOT demo so real-tenant pushes never blockdemo-tenant.service.spec.ts (7 specs) — PASSUS-009-004 (EP-009 D-008)
F10Cert v2 expires; v3 not yet provisionedAll outbound push fails with TLS handshake error; sync-status-strip flashes red on all connections; FU-6 runbook firesManual — covered by FU-6 runbook + monitoring/runbooks/resavenue-cert-reissuance.mdUS-009-005 (EP-009 D-009)

Day-2 channel operations (NOT in BF-007 scope)​

  • OTA messaging API integration — Booking.com Messages API, Expedia Guest Messaging, MMT chat. Pre-arrival upsell via OTA-owned relay channel. Strictly opt-in per OTA.
  • Rate parity monitoring v2 — real OTA scraping (Genius API + Agoda Partner Hub) replacing simulated random diffs (EP-009 D-013).
  • OTA promotion sync — <Promotion> extension upstream wiring (EP-009 D-010) — fetch from PromotionServiceClient.getApplicablePromotions and attach before push.
  • Commission tracking — per-channel commissionPct config + BookingCommission per booking + monthly GSTR-2B-ready CSV (EP-009 D-014, US-009-013).
  • TDS Section 194-O withholding — 1% TDS deducted on OTA payout when remitting to OTA in India. Channel report split: grossRevenue, commissionAmount, gstOnCommission (18% reverse-charge), tdsWithheld, netPayout.
  • VCC settlement for OTA no-shows — channel.noshow.reported publish back to OTA for Booking.com / Expedia VCC settlement (EP-003 D-010 proposal under US-003-010).
  • GDS direct (Amadeus / Sabre) — GdsModule import + SiteMinder / RateGain middleware (US-009-011 — currently flagged dead-code in review file pending re-verification).
  • Rate-parity enforcement automation — rate-parity-enforcement.service.ts exists but is mostly Best-Rate-Guarantee scaffolding; auto-corrective-push on violation is a Day-2 add.
  • Multi-currency push — Booking.com / Agoda accept per-OTA currency; today all push INR (EP-009 D-006 only covers GST mode, not currency).
  • Restriction model first-class — ChannelRestriction table + per-OTA capability map (EP-009 D-011 proposes new story US-009-NEW-RESTRICTIONS-FIRST-CLASS).

Indian-SME compliance overlay​

ConcernToday's behaviourDrift
GST on OTA commission (18% reverse charge — Booking.com / MMT / Agoda commission invoice carries IGST that the hotel must self-account)NOT in channel performance reportEP-009 D-014 (P0)
TDS Section 194-O (1% TDS withheld on OTA gross for e-commerce operators)TdsEntry model exists in schema; not surfaced in any reportEP-009 D-014 (P0)
GST mode per channel (Booking.com=GROSS / Expedia=NET / MMT=GROSS)No ChannelRateMapping.pricePushMode column; push assumes GROSSEP-009 D-006 (P1)
ResAvenue cert cycle (FU-6)Cert v2 active; v3 re-issuance against EKS ALB in flight; no automated cert-replay gateEP-009 D-009 (P0)
VCC settlement for OTA no-showsprocessNoShow() doesn't publish back to channel; OTA never gets the "no-show" signal needed to debit VCCEP-003 D-010 (P1, under US-003-010)
OTA-relay phone privacy (Booking.com Phone Privacy)Stored as Guest.phone; SMS dispatch tries to text the relay numberNEW drift candidate (BF-007 D-009 below)
DPDPA consent on OTA guest profileNo marketing-consent capture; OTA ownsAcceptable per partner contract; documented

Drift summary​

The 11 BF-007-surfaced drifts. Cross-references existing per-EPIC D-NNN IDs where the drift was already surfaced by EP-009 or EP-003 RTMs.

IDTypeSeverityDescriptionCross-refSurfaced by step
BF-007 D-001Cross-tenant securityP0ChannelConnection.create() trusts dto.propertyId unchecked — cross-tenant pricing-leak surface. Orphan denial spec covers READ side only; CREATE side unguarded.EP-009 D-001Step 3 / Setup
BF-007 D-002Credential exposureP0GET /channels/:id returns DECRYPTED credential payload in full — no masking. Combined with missing role guard on POST/PUT/PATCH, any authenticated user can exfiltrate OTA API keys.EP-009 D-002Step 5 / Setup
BF-007 D-003Cert lifecycleP0No automated CI gate replays a representative cert-cycle fixture (resavenue-cert-cycle.flow.spec.ts) on RESAVENUE_CERT_PATH rotation. Cert v3 cutover (FU-6) risks silent breakage.EP-009 D-009Step 5 + Step 7
BF-007 D-004India tax complianceP0Channel performance report carries no GST-on-OTA-commission (18% reverse charge) and no TDS Section 194-O (1% withholding) breakdown. TdsEntry schema exists; not surfaced.EP-009 D-014Step 6 (post-push reporting)
BF-007 D-005Catalog gap / India-distributionP1Allocation modes (POOL / RATIO / MANUAL — AP-T11-013) are shipped but not catalogued. RM teams critically need RATIO during Diwali / wedding season.EP-009 D-008Step 3
BF-007 D-006Story-incomplete / GST per channelP1ChannelRateMapping.pricePushMode ENUM('GROSS','NET') proposed but unbuilt; today all push assumes GROSS, breaking Expedia parity.EP-009 D-006Step 4
BF-007 D-007Story-incomplete / push debounceP1No debounce / coalesce window on auto-publish. A power-user editing 5 cells in 30s produces 5 distinct pushes; risks ResAvenue rate-limit + churn.NEW (BF-007 only)Step 2
BF-007 D-008OTA cancel double-chargeP1ChannelBookingCancelledConsumer ignores event.cancellationCharge; local policy applies on top → double-charge risk for OTAs that already VCC-debited.EP-003 D-007Step 10 (cancellation arm)
BF-007 D-009Privacy / OTA-relay phoneP2OTA-relay phone (Booking.com Phone Privacy) stored as Guest.phone; SMS dispatch attempts to text the relay number → silent failure or pollution of relay address book.NEW (BF-007 only)Step 11
BF-007 D-010Catalog gap / comms boundaryP2The "HMP suppresses guest comms for OTA-sourced reservations" policy is shipped behaviour but uncatalogued. Risk: a future contributor adds a new consumer that doesn't gate on source, sends emails through Booking.com relay, hotel gets delisted.NEW (BF-007 only)Step 12
BF-007 D-011OTA modification terminal-stateP2OTA-driven modifications on terminal-state reservations (CHECKED_IN / CHECKED_OUT / CANCELLED / NO_SHOW) log WARN + skip silently. Real ops want a manual-reconciliation queue with front-desk task.EP-009 D-012Step 10 (modification arm)

Cross-EPIC narrative summary​

StepSpansWhy cross-EPIC
1 → 2EP-011 (RM dashboard surface) ↔ EP-009 (channel-service receiver) ↔ EP-002 (property-service ARI cell owner)ARI Grid is a three-service write — UI → property cell → channel fanout.
3 → 6EP-009 (channel-service) + EP-015 (observability — sync logs)Outbound push lives entirely inside channel-service but its audit log is consumed by EP-015 monitoring.
7 → 8EP-009 (webhook receiver + HMAC + IP allowlist)Cert-contract surface — partner-facing.
9 → 10EP-009 (normalize) ↔ EP-003 (reservation create)The canonical-event seam — channel-service produces, reservation-service consumes. Schema drift here = duplicate reservations or void mappings.
11EP-003 (reservation create) ↔ EP-004 (guest match-or-create)Inline-guest path; OTA-relay phone privacy applies.
12EP-003 (reservation confirmed) ↔ EP-012 (notification)Comms-suppression boundary — OTA owns guest comms; HMP only fires internal ops notifications.

The "ARI cell write → distribution fanout → per-channel transform → ResAvenue XML → ChannelSyncLog" outbound spine and the "ResAvenue webhook → normalize → canonical event → reservation create → guest match → comms suppressed" inbound spine are the two seams BF-007 has to narrate. Both span 4+ services. Cert lifecycle (FU-6) and Indian-tax overlay (GST + TDS) are the primary BF-007 findings the SME must act on.