EP-009 — Channel Manager & Distribution RTM
EPIC: EP-009 Channel Manager & Distribution Reconciled: 2026-05-25 (Wave 1 of per-EPIC RTM scale-out) Template: EP-002-rtm.md; pattern: PLAYBOOK.md. Source of truth for "% passing":
hmp-docs/docs/platform-registry/2026-05-16-13-flow-demo-readiness.mduntil next re-measure.
Status legend
| Status | Meaning |
|---|---|
Built | Code exists + tests exist + tests pass |
Built-Untested | Code exists, no automated test coverage (or test exists but is a stub) |
Partial | Code exists for some acceptance criteria, gaps for others |
Stub | Code is a placeholder / TODO / returns mock data |
Not Started | No code |
Catalog numbering note:
01-epic-catalog.mdlists 13 stories for EP-009 (US-009-001..US-009-013); the EPIC body (epics/EP-009-channel-management.md) still uses the olderUS-009-01..US-009-13numbering AND has a different story title set (catalog splits Room Type Mapping / Rate Plan Mapping into two stories; EPIC body fuses them into one US-009-02 and shifts every downstream ID by one). Same drift pattern as EP-002 D-007. This RTM follows the catalog numbering. Per-storyreviews/EP-009/US-009-NNN-review.mdfiles (11 of them) also use the catalog three-digit format.
Summary
| Metric | Count |
|---|---|
| Total stories | 13 |
| Catalog claims "Built" | 4 (US-009-001, -002, -003, -012) |
| Verified-Built | 1 (US-009-012 sync-error monitoring; the three "Built" mapping stories are actually Partial once the SME pass classifies missing-RBAC + missing-credential-validation + missing-route-coverage drifts) |
| Built-Untested | 0 |
| Partial | 9 (US-009-001, -002, -003, -004, -005, -006, -009, -010, -011) |
| Stub | 1 (US-009-007 — restriction push delivered as a side-effect of pushAvailability, no dedicated endpoint, no MinLOS/MaxLOS storage) |
| Not Started | 2 (US-009-008 OTA modification/cancel webhooks, US-009-013 commission tracking) |
| Drift findings | 14 (D-001..D-014; 6 P0 / 6 P1 / 2 P2) |
| [SME-PROPOSED] refinements | 25 |
US-009-001 — Channel Connection Setup (Credentials & Config)
Catalog status: Built (P0, 5 points)
Verified status: Partial — CRUD shipped with AES-256-GCM encryption; missing role guards, audit trail, credential masking, and createConnection does not validate the propertyId FK against the JWT tenant (Task #11 security gap)
Code locations:
hmp-channel-service/src/connections/connections.service.ts:172-206—create(tenantId, dto)validates OTA-schema credentials, encrypts viaFieldEncryptionService, stampstenantIdfrom the caller but trustsdto.propertyIdwith no FK lookuphmp-channel-service/src/connections/connections.controller.ts— REST surface:GET /channels,GET /channels/:id,POST /channels,PUT /channels/:id,PATCH /channels/:id/deactivate,GET /channels/:id/health,GET /channels/ota-schemashmp-channel-service/src/connections/ota-schemas.ts— Per-channel credential field requirements (Booking.com, MakeMyTrip, Expedia, Agoda, Goibibo, etc.) +validateOtaCredentials()helperhmp-channel-service/src/connections/webhook-registration.service.ts—registerWebhookWithResAvenue(connectionId)(auto-fires when mode flips LIVE)hmp-channel-service/src/intermediary/intermediary.controller.ts—POST /intermediary/connections(now@Roles('SUPER_ADMIN')after AP-T11-024 Phase 2.5)hmp-channel-service/src/intermediary/intermediary.service.ts— credentials JSON shape (includeswebhookCallbackUrl,webhookSecret,whitelistedIps,outboundIp,endpoint,providerIdFK)hmp-web-dashboard/src/app/(dashboard)/settings/channels/page.tsx— Channel status grid landinghmp-web-dashboard/src/app/(dashboard)/settings/channels/managers/page.tsx— Intermediary manager admin
Unit + integration tests:
hmp-channel-service/src/connections/connections.service.spec.ts— 4 tests (create connection, room mapping, health check, not-found); does NOT cover update, deactivate, list, duplicate handling, encryption round-triphmp-channel-service/src/connections/ota-schemas.spec.ts— per-channel credential validationhmp-channel-service/src/connections/dto/connection.dto.spec.ts— DTO validationhmp-channel-service/src/intermediary/__tests__/connection-cross-tenant-denial.spec.ts— 11 tests including the explicit "encryption.decrypt is never called for foreign-tenant rows" assertion (Task #2 deliverable). Status: orphaned (no live consumer registers the spec into a CI gate; passes today only becauseIntermediaryService.findOnehappens to filter by tenant)hmp-channel-service/src/intermediary/__tests__/intermediary-role-gate.spec.ts— 10 cases locking the AP-T11-024 Phase 2.5 SUPER_ADMIN gate on intermediary POST/PATCH
Flow tests:
hmp-infrastructure/testing/flow-tests/src/flows/channel-sync.flow.spec.ts:: "channel connection bootstrap" — TBD (not in 13-flow doc) (2026-05-16-13-flow-demo-readiness.md, 2026-05-16)hmp-infrastructure/testing/flow-tests/src/flows/channel-manager-multi.flow.spec.ts:: "multi-OTA seed end-to-end" — TBD (not in 13-flow doc) (2026-05-16-13-flow-demo-readiness.md, 2026-05-16)
Real-backend smokes:
hmp-web-dashboard/e2e/real-backend/smoke-channel-connect.real.spec.ts— Channel connect happy path
E2E (Playwright):
- (no dedicated Playwright; covered via smoke + ARI dashboard tests)
Use cases referencing this story:
- none
SME notes:
- AES-256-GCM encryption on
credentialsJSON is the right call (FieldEncryptionService) — but the response path onGET /channels/:idDECRYPTS and returns the full secret. There is no masking. A frontend bug or a screen-share leak exposes the raw OTA API key. createConnectionstampstenantIdfrom the caller but trustsdto.propertyIdas-is. There is noassertPropertyTenant(propertyId, tenantId)check — a malicious tenant could supply another tenant's propertyId and the row would be created with crossed wires (caller's tenantId + victim's propertyId). The orphanconnection-cross-tenant-denial.spec.tscovers the READ side only. This is the Task #11 security gap and is P0 by SME hat — pricing data leak + cross-tenant rate-push collision.- Channel-specific credential validation is in
ota-schemas.ts(Booking.com needs hotelId+apiKey, Agoda needs apiKey+propertyId, etc.) buttestConnection()returns a hardcodedstatus: 'healthy'with a random latency — does NOT actually call the OTA. AC-4 in the review file calls this out. - ResAvenue intermediary credentials seeded in QA include the rotated 2026-05-04 user
testavpms@resavenue.com(AVNPMScontext) — wastestpmsk4@resavenue.com/JTPMS. Cert process is in flight (FU-6 inhmp-infrastructure/compliance/follow-ups.md). - No
AuditLogwrite on credential create/update/deactivate. IT Act 43A + DPDPA breach-notification both expect a tamper-evident trail of credential mutations.
Drift findings:
D-001 — [P0] [Security + missing-validation] ConnectionsService.create() stamps tenantId from req.user.tenantId but trusts dto.propertyId unchecked. No FK call to PropertyServiceClient.getProperty(propertyId) to verify the property belongs to the caller's tenant. Combined with the same propertyId-as-input pattern on the channel-mappings + sync endpoints, this is the cross-tenant pricing-leak surface the Phase 1 ServiceClient migration (cross-service-communication.md) explicitly mandates assertPropertyTenant for. The orphan connection-cross-tenant-denial.spec.ts (11 tests) covers READ-side denial only; CREATE-side is unguarded.
- [SME-PROPOSED] Add AC: "
POST /channelsSHALL callPropertyServiceClient.assertPropertyTenant(dto.propertyId, req.user.tenantId)BEFORE write. On mismatch return 403 withcode=CROSS_TENANT_PROPERTY_DENIED. Test asserts a foreign-tenant propertyId is rejected before any DB write." - [SME-PROPOSED] Promote
connection-cross-tenant-denial.spec.tsfrom orphan status: wire into the CI suite undernpm testand add the matching CREATE-side test cases (currently 11 tests for findOne/findAll/getRoomMappings/getRateMappings; need a 12th for create + 13th for update).
D-002 — [P0] [Story-incomplete + security] No credential masking in GET /channels/:id response — the decrypted secret is returned in full. No role-based guard on POST/PUT/PATCH /channels/* (the controller-level JwtAuthGuard accepts any authenticated user). Reviews confirm both (GAP-2, GAP-4). Combined surface = an authenticated frontdesk-agent (typo URL) sees and exfiltrates the property's Booking.com API key.
- [SME-PROPOSED] Add AC: "Channel CRUD mutation endpoints (
POST/PUT/PATCH /channels/*) SHALL require@Roles('MANAGER', 'REVENUE_MGR', 'ADMIN', 'SUPER_ADMIN').GET /channels/:idSHALL mask credential fields to***last4for non-SUPER_ADMIN callers; SUPER_ADMIN gets the full decrypted value with anAuditLogwrite recording the read." - [SME-PROPOSED] Add AC: "Every credential mutation (
create/update/deactivate) SHALL write anAuditLogrow{actor, channelCode, fieldsChanged[], at}— required by IT Act 43A (reasonable security practices) and DPDPA breach-trace mandates."
US-009-002 — Room Type Mapping to OTA Channels
Catalog status: Built (P0, 5 points) Verified status: Partial — CRUD shipped; no canonical OTA-code taxonomy, no "many-to-one PMS to OTA" guard
Code locations:
hmp-channel-service/src/connections/connections.service.ts—createRoomMapping(channelId, dto)writesChannelRoomMappinghmp-channel-service/src/connections/connections.controller.ts—POST /channel-mappings/rooms/:channelId,GET /channel-mappings/rooms/:channelIdhmp-channel-service/src/intermediary/intermediary.controller.ts—IntermediaryMappingrows (3 room types seeded in ResAvenue PMS Test Hotel)hmp-channel-service/src/connections/connections.service.ts::hydrateRoomMapping(lines 38-45) — service-client lookup against property-serviceRoomType(replaces deleted Prisma relation)hmp-web-dashboard/src/app/(dashboard)/settings/channels/mappings/page.tsx— Mappings UI
Unit + integration tests:
hmp-channel-service/src/connections/connections.service.spec.ts— 1 test (createRoomMapping happy path)
Flow tests:
hmp-infrastructure/testing/flow-tests/src/flows/channel-sync.flow.spec.ts— TBD (2026-05-16-13-flow-demo-readiness.md)hmp-infrastructure/testing/flow-tests/src/flows/channel-manager-multi.flow.spec.ts— TBD (2026-05-16-13-flow-demo-readiness.md)
Real-backend smokes: none specific
Use cases referencing this story: none
SME notes:
- OTA room-type codes are free-form
String— channel-service holds whatever the operator types. The seed (seed-resavenue-firsthotel-connection.sql) hardcodes 3 ResAvenue room types but there is no per-OTA validation that the supplied code resembles their format. Booking.com room IDs are numeric; Agoda uses GUIDs; MakeMyTrip usesMMT-XXX-NNNpatterns — today a typo silently maps to a non-existent OTA room. - "Many PMS to one OTA" + "one PMS to many OTAs" are both allowed by schema. The reverse direction (one OTA room mapped to two PMS room types) creates a fan-out ambiguity on booking pull — the
BookingDownloadCronwould have to pick one. No DB constraint blocks it. hydrateRoomMappingswallows property-service failures and returnsroomType: null. Mappings become invisibly broken when the upstream room type is deleted in property-service — story does not require an integrity check or alert.
Drift findings:
D-003 — [P1] [Story-incomplete + India-OTA-integration-risk] No per-OTA room-code format validation. Operator types MMT-DLX-O1 instead of MMT-DLX-01 (letter O vs digit zero) and the rate push silently lands on a non-existent OTA room. No bookings come back because the OTA never had inventory under that code. Symptom: "OTA is broken" — actual root cause: typo. Same class of bug as the ResAvenue <OTAName> saga (CLAUDE.md 2026-05-05).
- [SME-PROPOSED] Add AC: "Room/rate mapping create + update SHALL validate
otaRoomTypeCode/otaRateCodeagainst a per-OTA regex (Booking.com:^\\d+$, Agoda: 36-char GUID, MakeMyTrip:^MMT-[A-Z]{3}-\\d{2,3}$, etc.). Unknown OTA codes warn (not block) so we can adapt to provider format changes; format violations on KNOWN OTAs return 400 with the example pattern."
D-004 — [P2] [Orphaned-code + integrity] hydrateRoomMapping silently returns roomType: null when the upstream room is gone. Mappings become "ghost mappings" pointing at deleted PMS room types — pushes continue to land at the OTA with stale codes, bookings pull back into the void.
- [SME-PROPOSED] Add AC: "A daily integrity-check cron SHALL list mappings whose
roomType/ratePlanhydration returns null and publishchannel.mapping.brokenfor ops alerting + dashboard flag. Pause auto-sync for affected channels until reconciled."
US-009-003 — Rate Plan Mapping to OTA Channels
Catalog status: Built (P0, 5 points) Verified status: Partial — CRUD + derived-formula engine shipped; same orphaned-code + missing-validation gaps as US-009-002; STD-CP-as-child-rate gotcha (2026-05-05) unresolved
Code locations:
hmp-channel-service/src/connections/connections.controller.ts—POST /channel-mappings/rates/:channelId,GET /channels/rate-mappings,PUT /channels/rate-mappings/:id,POST /channels/rate-mappings/calculatehmp-channel-service/src/rate-mapping/*—ChannelRateMapping.formulaengine for derived rates (e.g.parent + 5%for breakfast variant)hmp-channel-service/src/connections/connections.service.ts::hydrateRateMapping(lines 51-58) — service-client lookup against property-serviceRatePlanhmp-web-dashboard/src/app/(dashboard)/settings/channels/mappings/page.tsx
Unit + integration tests:
hmp-channel-service/src/rate-mapping/*.spec.ts[CITED-BUT-NOT-FOUND]— rate formula engine tests (filesystem listing did not surface per-file specs in rate-mapping dir; verify Task 6)
Flow tests: none specific (covered via channel-sync flow)
Real-backend smokes: none
Use cases referencing this story: none
SME notes:
- ResAvenue's PMS Test Hotel has STD-CP set as a child rate of STD-EP (parent) — pushing CP returns
Invalid xml request. Child rate updates are not allowed. Please try to update Parent rate(s) only.(CLAUDE.md 2026-05-05). Today we have no metadata onChannelRateMappingindicating which mapped codes are partner-side children — every cert run requires manual investigation. - Derived-formula engine is the right primitive for "CP = EP + 350 INR breakfast", but no UI affordance for "this rate is locked as a derivative on partner side, do NOT push directly".
- GST-inclusive vs GST-exclusive resolution per channel: OTAs differ (Booking.com expects inclusive in India, Expedia historically expected exclusive). Story does not state the resolution rule. Cross-references EP-002 D-008 (rate plan GST mode).
Drift findings:
D-005 — [P1] [Cross-OTA-config + cert-blocker] ChannelRateMapping carries no flag for "this OTA rate code is a child / derived rate on partner side and must NOT be pushed". Cert evidence (KNV_R_06 STD-CP) failed for this exact reason 2026-05-05.
- [SME-PROPOSED] Add column
ChannelRateMapping.partnerSideKind ENUM('PARENT', 'CHILD_DERIVED', 'INDEPENDENT')(default INDEPENDENT).SyncService.pushRatesSHALL skip rows withCHILD_DERIVEDand logRATE_SKIPPED_PARTNER_CHILDinstead of letting them 400. UI surfaces the kind in the mapping grid.
D-006 — [P1] [Story-incomplete + India-hospitality-critical] Story is silent on GST-inclusive vs GST-exclusive resolution per channel. EP-002 D-008 calls for RatePlan.pricingMode; channel mapping needs the inverse — a ChannelRateMapping.pricePushMode ENUM('GROSS', 'NET') indicating whether the OTA wants inclusive or exclusive of GST.
- [SME-PROPOSED] Add AC: "
ChannelRateMapping.pricePushMode(GROSSincludes GST;NETexcludes).SyncService.pushRatesSHALL apply or strip GST per the mapping value before dispatching to the adapter. Default per channel: Booking.com=GROSS, Expedia=NET, MakeMyTrip=GROSS, Agoda=GROSS, Goibibo=GROSS."
US-009-004 — Availability Push to OTAs (Real-Time)
Catalog status: Partial (P0, 8 points)
Verified status: Partial — manual single-channel push works; auto-trigger Kafka consumer + fan-out shipped (ChannelAutoPushConsumer), but retry + debounce + allocation-mode coupling partial
Code locations:
hmp-channel-service/src/sync/sync.controller.ts—POST /channel-sync/push-availability,POST /channel-sync/push-rates,POST /channel-sync/push-restrictions,POST /channel-sync/sync-all,GET /channel-sync/logshmp-channel-service/src/sync/sync.service.ts—pushAvailability(channelId, items); allocation-policy clamp viaAllocationService.resolveCap()per AP-T11-013; demo-tenant guard short-circuit per Plan 4 (2026-05-13); idempotencyKey persistence per AP-T11-001 (2026-05-08); per-payload SHA-256 deduphmp-channel-service/src/sync/channel-auto-push.consumer.ts— Kafka consumer for reservation/folio events that fan out to all active connectionshmp-channel-service/src/sync/sync-retry.consumer.ts— Retry-after-failure consumerhmp-channel-service/src/sync/parallel-sync.service.ts— Parallel fan-out across connectionshmp-channel-service/src/sync/booking-download.cron.ts— Inverse direction; cron-pulls bookingshmp-channel-service/src/distribution/allocation.service.ts—resolveCap(propertyId, roomTypeId, date, channelCode)clamps push value before adapter call (AP-T11-013 POOL/RATIO/MANUAL)hmp-channel-service/src/demo-tenant/demo-tenant.service.ts— Plan 4 demo-tenant guard with 5-min Redis cachehmp-channel-service/src/internal/*—POST /internal/channel/distribution-fanout(AP-T11-011 distribution-fanout receiver from property-service)hmp-property-service/src/distribution/distribution.service.ts— Owner of ARI cell writes; callsChannelServiceClient.distributionFanout()after cell savehmp-property-service/src/distribution/aggregator.service.ts— Reads allocations intoAriCellState.allocationshmp-web-dashboard/src/components/modules/ari/ari-grid.tsx+ 18 sibling files — ARI grid UI (cell, channel-override-dialog, closure-dialog, dow-pattern-dialog, sync-status-strip, history-drawer)
Unit + integration tests:
hmp-channel-service/src/sync/sync.service.spec.ts— 14 tests including AP-T11-001 idempotencyKey 64-char hex + retry-stability + value-change-changes-keyhmp-channel-service/src/distribution/__tests__/allocation.service.spec.ts— AP-T11-013 POOL/RATIO/MANUAL clamphmp-channel-service/src/demo-tenant/demo-tenant.service.spec.ts— 7 specs (Plan 4 demo guard)hmp-property-service/src/distribution/__tests__/distribution.service.spec.ts— ARI cell write + fanouthmp-property-service/src/distribution/__tests__/aggregator.allocation.spec.ts— AP-T11-013 cell aggregationhmp-property-service/src/distribution/__tests__/cell-writer.service.spec.ts— cell write idempotency
Flow tests:
hmp-infrastructure/testing/flow-tests/src/flows/ari-grid.flow.spec.ts:: "ARI grid push lifecycle" — TBD (not in 13-flow doc) (2026-05-16-13-flow-demo-readiness.md, 2026-05-16)hmp-infrastructure/testing/flow-tests/src/flows/channel-sync.flow.spec.ts:: "push-availability lifecycle" — TBD (2026-05-16-13-flow-demo-readiness.md)hmp-infrastructure/testing/flow-tests/src/flows/channel-manager-multi.flow.spec.ts:: "multi-channel fan-out" — TBD (2026-05-16-13-flow-demo-readiness.md)
Real-backend smokes:
hmp-web-dashboard/e2e/real-backend/smoke-channel-connect.real.spec.ts
E2E (Playwright):
hmp-web-dashboard/e2e/ari-grid.spec.tshmp-web-dashboard/e2e/distribution-redirects.spec.tshmp-web-dashboard/e2e/tests/channel-push-ops.spec.ts
Use cases referencing this story: none
SME notes:
- The current implementation is MUCH closer to spec than the 2026-03-07 review file claims (review verdict was FAIL / 1-of-10). Since then:
ChannelAutoPushConsumer+SyncRetryConsumer+ParallelSyncService+ AP-T11-013 allocation clamping all shipped. Review file is materially stale — same drift class as EP-002 D-011. - AP-T11-013 allocation modes (POOL / RATIO / MANUAL) are the Indian-market answer to channel-distribution gaming. POOL = open inventory to all OTAs (last-write-wins); RATIO = "30% Booking.com, 20% MMT, 50% direct" weighted distribution; MANUAL = operator sets explicit per-channel caps. Story body says nothing about this distinction.
- Demo-tenant guard (Plan 4) short-circuits push to live OTAs for tenants flagged
isDemoTenant=true— critical because a sandbox tenant accidentally pushing junk rates to real Booking.com breaks parity for every neighbor on the channel. Story should reference this. - ResAvenue
<OTAName>master-vs-channel-wise distinction (CLAUDE.md 2026-05-05): outbound XML omits<OTAName>by default for master-update (works); only emits per-channel whencredentials.emitOtaName=true. Today this is a credential-level toggle, not surfaced anywhere in the story.
Drift findings:
D-007 — [P0] [Review-vs-code] reviews/EP-009/US-009-004-review.md verdict FAIL / 1-of-10 / "no Kafka consumer / no fan-out / no retry" is stale (2026-03-07). All three shipped via ChannelAutoPushConsumer + SyncRetryConsumer + ParallelSyncService, plus AP-T11-001/-011/-013 layered on top. Anyone reading the review alone would re-open closed work.
- [SME-PROPOSED] Re-run the US-009-004 review; refresh verdict to PARTIAL (auto-trigger + fan-out + retry shipped; debounce window + persistent-failure NOTIFICATION_SEND publish still gaps; CTA/CTD/MinLOS surface goes through
pushAvailabilityrather than the dedicatedpushRestrictionsendpoint US-009-007 expects).
D-008 — [P1] [Story-incomplete + India-distribution-critical] Story is silent on allocation modes (POOL/RATIO/MANUAL — AP-T11-013). Indian RM teams critically depend on RATIO mode during Diwali / wedding season to cap each OTA's share — without it, MakeMyTrip will pull 80% of inventory at peak and crowd out direct + Booking.com.
- [SME-PROPOSED] Add AC: "Channel allocation policies SHALL support modes POOL (open), RATIO (weighted per channel), MANUAL (operator-set per-channel caps) at (property, roomType, date) granularity.
SyncService.pushAvailabilitySHALL clamp the per-channel push value viaAllocationService.resolveCap()before adapter dispatch. Default mode POOL is no-op; missing rows are no-op." - [SME-PROPOSED] Add AC: "Demo / sandbox tenants (
Tenant.isDemoTenant=true) SHALL be short-circuited at push time withBadRequestException('DEMO_TENANT_OTA_BLOCKED')and a WARN log. Fail-safe: auth-service outage → guard treats tenant as NOT demo so real-tenant pushes never get blocked by transient infra."
US-009-005 — Booking Pull from OTAs (New Reservations)
Catalog status: Partial (P0, 8 points) Verified status: Partial — webhook ingestion + canonical normalizer + duplicate detection shipped for ResAvenue; cron-based pull still mock for non-ResAvenue OTAs; modification/cancellation routing partial
Code locations:
hmp-channel-service/src/intermediary/providers/resavenue/resavenue-webhook.controller.ts—POST /webhooks/resavenue/booking|reservation|cancellation+ HMAC/push(AP-T11-007 single inbound URL, AP-T11-007 dual-auth Classic + UserName/Password fallback)hmp-channel-service/src/intermediary/providers/resavenue/resavenue-xml.parser.ts— XML parser with multi-RoomStay fix (2026-05-05); also JSON parser pathhmp-channel-service/src/intermediary/channel-booking-normalizer.service.ts—ChannelBookingNormalizer.normalize{Reservation,Cancellation,Modification}producing canonicalOtaBookingReceivedEvent/OtaBookingCancelledEvent(@hmp/api-contracts@1.9.4)hmp-channel-service/src/intermediary/abstract-intermediary-webhook.controller.ts— Base webhook controller: IP allowlist (ENFORCE/SHADOW/OFF per AP-T11-024 Phase 4E), credential extraction, raw-payload persist toWebhookEventhmp-channel-service/src/sync/booking-download.cron.ts— Cron-based pull for non-ResAvenue OTAs (mock today)hmp-reservation-service/src/consumers/channel-booking-received.consumer.ts— Canonical event consumer, creates reservationhmp-reservation-service/src/consumers/channel-booking-cancelled.consumer.ts— Cancellation consumerhmp-channel-service/src/webhooks/webhook-management.controller.ts—POST /webhook-management/events/:id/replay(SUPER_ADMIN-only, AP-T11-007 follow-up 2026-05-05)
Unit + integration tests:
hmp-channel-service/src/intermediary/providers/resavenue/__tests__/resavenue-webhook-booking.spec.ts— booking webhook ingestionhmp-channel-service/src/intermediary/providers/resavenue/__tests__/resavenue-webhook-hmac.spec.ts— HMAC/pushsignature validationhmp-channel-service/src/intermediary/providers/resavenue/__tests__/resavenue-xml.parser.spec.ts— XML parser (multi-RoomStay coverage)hmp-channel-service/src/intermediary/providers/resavenue/__tests__/resavenue-cert-format.spec.ts— Cert-format gates (15 cases)hmp-channel-service/src/intermediary/__tests__/abstract-intermediary-webhook.spec.ts— 14 specs for dual-auth + IP allowlist brancheshmp-channel-service/src/intermediary/__tests__/channel-booking-normalizer.spec.ts— canonical event shapehmp-channel-service/src/webhooks/__tests__/webhook-management-replay.spec.ts— Replay endpoint
Flow tests:
hmp-infrastructure/testing/flow-tests/src/flows/resavenue-cert-cycle.flow.spec.ts:: "RA cert cycle end-to-end" — TBD (not in 13-flow doc) (2026-05-16-13-flow-demo-readiness.md, 2026-05-16)hmp-infrastructure/testing/flow-tests/src/flows/channel-manager-multi.flow.spec.ts:: "multi-channel booking-pull" — TBD (2026-05-16-13-flow-demo-readiness.md)
Real-backend smokes:
- Implicit via
smoke-channel-connect.real.spec.ts
E2E (Playwright):
hmp-web-dashboard/e2e/tests/channel-push-ops.spec.ts(covers reverse-direction visibility)
Use cases referencing this story: none
SME notes:
- ResAvenue is the only OTA today with a real bidirectional webhook path. Booking.com / Expedia / MakeMyTrip / Agoda / Goibibo all flow THROUGH ResAvenue (
routedVia: RESAVENUE) — they have no direct webhook handlers because we don't have certs for those. - IP allowlist with three modes (ENFORCE / SHADOW / OFF — AP-T11-024 Phase 4E) is a hospitality-grade safety net. SHADOW mode is the unsung hero: lets operators preview enforcement impact before flipping the gate. Story body doesn't mention modes.
WebhookEventrawPayload persistence + replay endpoint (2026-05-05) closed a real prod-recovery gap from the multi-RoomStay parser bug. Story should require this.- Cert FU-6 (RA re-issuance against EKS-fronted ALB) is in flight; until v3 cert lands, RESAVENUE_CERT_PATH points at the v2 (2026-05-05) cert. Spec must reference this lifecycle.
Drift findings:
D-009 — [P0] [Story-incomplete + cert-lifecycle] Story has no acceptance criteria for the ResAvenue cert cycle (FU-6) or the IP allowlist enforcement modes (ENFORCE/SHADOW/OFF). Both are real production safety surfaces — losing either silently is a SEV-2 incident.
- [SME-PROPOSED] Add AC: "ResAvenue inbound webhooks SHALL persist
WebhookEvent.rawPayloadfor every accepted AND rejected request. SUPER_ADMIN-onlyPOST /webhook-management/events/:id/replaySHALL re-POST the raw bytes withX-Forwarded-Forset to a connection-allowed IP, persisting a NEW audit row (original event preserved)." - [SME-PROPOSED] Add AC: "IP allowlist enforcement SHALL support three modes (
ENFORCEreject 403 + audit,SHADOWlog + WARN + accept + audit,OFFskip).IntermediaryConnection.enforcementModeOverridewins overIntermediaryProviderConfig.enforcementMode. DefaultENFORCE." - [SME-PROPOSED] Add AC: "Cert lifecycle: when
RESAVENUE_CERT_PATHrotates, an automated test SHALL replay a representative cert-cycle fixture (resavenue-cert-cycle.flow.spec.ts) and gate the deploy on its success."
US-009-006 — Rate Push to OTAs
Catalog status: Partial (P1, 8 points)
Verified status: Partial — single + bulk push shipped with 3-retry backoff + idempotencyKey; ResAvenue spec v1.7 alignment shipped (<OTAName> omit-default, <Promotion>, single-pax adjustment, date-range collapse); multi-currency + GST-side resolution still gaps
Code locations:
hmp-channel-service/src/sync/sync.service.ts—pushRates(channelId, items), 3-attempt exponential backoff, idempotencyKeyhmp-channel-service/src/sync/dto/sync.dto.ts—RateItemDtowithdayOfWeek,minStay,maxStay,stopSell,singlePaxAdjustmenthmp-channel-service/src/intermediary/providers/resavenue/resavenue-xml.builder.ts— Spec v1.7 alignment:<OTAName>omit-default (AP-T11-007),<Promotion>(AP-T11-014), single-paxBaseByGuestAmtdivergence (AP-T11-015), date-range collapse (2026-05-05)hmp-channel-service/src/intermediary/providers/resavenue/resavenue.adapter.ts—pushRates+pushAvailability, populatesSyncResult.requestXml/responseXmlhmp-property-service/src/promotions/promotions.service.ts— Promotion CRUD; consumed viaPromotionServiceClient.getApplicablePromotions(wiring deferred per AP-T11-014)
Unit + integration tests:
hmp-channel-service/src/sync/sync.service.spec.ts— 14 tests covering pushRates + idempotencyKey + retryhmp-channel-service/src/intermediary/providers/resavenue/__tests__/resavenue-xml.builder.spec.ts— 71 specs incl.<Promotion>+ single-pax + date-range collapse +<OTAName>omit/emithmp-channel-service/src/intermediary/providers/resavenue/__tests__/resavenue.adapter.spec.ts— adapter push/pullhmp-property-service/src/promotions/__tests__/promotions.service.spec.ts
Flow tests: same as US-009-004 (channel-sync, channel-manager-multi, resavenue-cert-cycle)
Real-backend smokes: implicit via smoke-channel-connect
Use cases referencing this story: none
SME notes:
- Catalog says P1; given this is a P0-launch channel (the cert) downgrading was a mistake — should be P0.
- AP-T11-014
<Promotion>extension is the cleanest answer to Indian-market "Diwali special" / "wedding-season promo" push, but the wiring to actually fetch + attach promos fromPromotionServiceClientis deferred — today every rate push omits promotions. Story should explicit-spec this loop closure. - AP-T11-015 single-pax adjustment is the Indian market gold for solo-business-traveller pricing (single-occupancy 70% of double — vs Western OTAs which want pure double-base). Spec should require the field be exposed in the rate-plan UI.
- Multi-currency: today all push is INR. Booking.com expects per-OTA currency; story silent on multi-currency.
Drift findings:
D-010 — [P1] [Wiring-deferred + revenue-impact] AP-T11-014 <Promotion> outbound XML is shipped but the upstream wiring (call PromotionServiceClient.getApplicablePromotions(propertyId, ratePlanId, date, {channelId}) from SyncService.pushRates) is deferred. Every rate push to ResAvenue today omits the promo, so Diwali / wedding-season promos never reach Booking.com Genius / Expedia Member Deals through ResAvenue.
- [SME-PROPOSED] Add AC: "
SyncService.pushRatesSHALL fetch applicable promotions viaPromotionServiceClient.getApplicablePromotions(propertyId, ratePlanId, date, {channelId})and attach toRateUpdate.promotionbefore adapter dispatch. Caching: 5-min Redis TTL per (propertyId, ratePlanId, date, channelId). Test asserts a promo from property-service surfaces in the outbound XML."
US-009-007 — Restriction Push (Min Stay, CTA, CTD)
Catalog status: Not Started (P2, 5 points)
Verified status: Stub — POST /channel-sync/push-restrictions endpoint exists and delegates to pushAvailability (closure flags) or pushRates (stay-only); no dedicated channel_restrictions storage table, no per-OTA support matrix, no frontend page at /channel-manager/restrictions
Code locations:
hmp-channel-service/src/sync/sync.controller.ts—POST /channel-sync/push-restrictionshmp-channel-service/src/sync/sync.service.ts::handleRestrictionPush— splits restrictions: closure rows → inventory XML, stay-only rows → MUST go via pushRates (CLAUDE.md 2026-05-04 supersede)hmp-channel-service/src/intermediary/providers/resavenue/resavenue-xml.builder.ts— emits<MinStay>/<MaxStay>inside<Rate>(Q9b); does NOT emit<LengthsOfStay>(removed 2026-05-04 per ResAvenue Manish)
Unit + integration tests:
hmp-channel-service/src/intermediary/providers/resavenue/__tests__/resavenue-xml.builder.spec.ts— covers<LengthsOfStay>negative assertions + MinStay/MaxStay in<Rate>
Flow tests: implicit in channel-sync flow
Real-backend smokes: none
Use cases referencing this story: none
SME notes:
- The "Stub" classification reflects the gap between what the catalog wants (dedicated
channel_restrictionstable, per-OTA support matrix, frontend page) and what is shipped (a split-router method). Functionally restrictions work for ResAvenue; everywhere else is mock. - The 2026-05-04 ResAvenue supersede (drop
<LengthsOfStay>from inventory XML, use only<MinStay>/<MaxStay>in rate XML) is an OTA-specific quirk. Story should NOT bake the protocol detail in — it should require an adapter abstraction that lets each OTA route restrictions differently.
Drift findings:
D-011 — [P2] [Stub-shipped-as-Not-Started] Catalog says Not Started. Reality is a thin alias over pushAvailability + pushRates. Functionally usable for ResAvenue, but no operator UI, no channel_restrictions storage, no per-OTA capability map.
- [SME-PROPOSED] Bump status from
Not Started→Stuband author a new storyUS-009-NEW-RESTRICTIONS-FIRST-CLASS"Dedicated restriction model + per-OTA capability map + management UI". Acceptance:ChannelRestrictiontable;POST /channel-restrictions(bulk);GET /channel-restrictions?propertyId=&from=&to=; per-OTA capability map ({BCOM: ['CTA','CTD','MIN_LOS','MAX_LOS','STOP_SELL'], EXP: ['CTA','CTD','STOP_SELL']}); unsupported restrictions return 400 withcode=RESTRICTION_UNSUPPORTED_FOR_CHANNEL.
US-009-008 — Booking Modification/Cancellation from OTA
Catalog status: Not Started (P1, 5 points)
Verified status: Partial — cancellation path SHIPPED via canonical OtaBookingCancelledEvent (AP-T11-023, 2026-05-01); modification path SHIPPED via ChannelBookingReceivedConsumer.applyModification for date/pax/specialRequests; terminal-state guard logs WARN + skips; per-room rate/room-type changes out of scope for v1
Code locations:
hmp-channel-service/src/intermediary/channel-booking-normalizer.service.ts::normalizeCancellation— canonical event with schemaVersion 1.0hmp-channel-service/src/intermediary/providers/resavenue/resavenue-webhook.controller.ts::processCancellation— publishes canonical envelope tochannel.booking.cancelledhmp-reservation-service/src/consumers/channel-booking-cancelled.consumer.ts— subscriberId='channel-booking-cancelled-consumer'hmp-reservation-service/src/consumers/channel-booking-received.consumer.ts::applyModification— routesnotificationType: 'MODIFICATION'events toReservationsService.update(); terminal-state guard
Unit + integration tests: implicit in channel-booking-normalizer.spec.ts + the resavenue webhook specs
Flow tests: implicit in resavenue-cert-cycle.flow.spec.ts
Real-backend smokes: none
Use cases referencing this story: none
SME notes:
- Catalog "Not Started" is stale. The cancel + modify path closed via AP-T11-023 (2026-05-01) for ResAvenue.
- Per-room rate / room-type changes are out of scope for v1 — would need a per-room PATCH endpoint on reservation-service. Story should call this out.
- Modifications to terminal-state reservations (CHECKED_IN/CHECKED_OUT/CANCELLED/NO_SHOW) currently log + skip. Real ops want a "manual reconciliation queue" instead of a silent log entry.
Drift findings:
D-012 — [P0] [Catalog-vs-code] Catalog says Not Started; AP-T11-023 (2026-05-01) shipped the canonical cancel/modify pipeline for ResAvenue with consumer-side handlers in reservation-service. Reading the catalog alone, a new engineer would re-implement.
- [SME-PROPOSED] Refresh catalog status
Not Started→Partial. Add AC: "OTA-originated cancellations SHALL produce a canonicalOtaBookingCancelledEvent(schemaVersion 1.0) onchannel.booking.cancelled. OTA-originated modifications (date/pax/specialRequests only — per-room changes out of scope for v1) SHALL route viaChannelBookingReceivedConsumer.applyModification. Terminal-state reservations SHALL go to amanual-reconciliationqueue (replace the WARN + skip pattern) with a front-desk task auto-created."
US-009-009 — Rate Parity Alert Engine
Catalog status: Partial (P1, 5 points)
Verified status: Partial — ParityService + scheduled checks + Kafka alert publish + history shipped; uses simulated rate diffs (random ±2%), in-memory violation storage, no real OTA scraping, no severity classification, default 5% threshold vs spec 2%
Code locations:
hmp-channel-service/src/parity/parity.service.ts— Parity check enginehmp-channel-service/src/parity/parity.controller.ts—GET /channel-sync/parity,/parity/history,/parity/summary,POST /parity/enforce,GET /parity/checkhmp-channel-service/src/parity/parity-history.service.ts— In-memory historyhmp-channel-service/src/parity/rate-parity-enforcement.service.ts— Best Rate Guarantee enforcementhmp-channel-service/src/parity/parity-coverage.service.spec.ts— coverage spec
Unit + integration tests:
hmp-channel-service/src/parity/parity.service.spec.tshmp-channel-service/src/parity/parity-coverage.service.spec.tshmp-channel-service/src/parity/rate-parity-enforcement.service.spec.ts
Flow tests: none specific
Real-backend smokes: none
Use cases referencing this story: none
SME notes:
- "Simulated rates" is the core gap. The whole point of parity monitoring is to compare PMS-pushed-rate vs OTA-published-rate (which can drift due to OTA-side promotions, GenusBOOST, MMT-couponing, etc.). Today we compare pushed-vs-pushed — that's only useful for detecting our OWN multi-channel divergence (RATE_DIFFERS_BY_CHANNEL bugs in
SyncService). - True parity needs ONE OF: (a) OTA rate-reporting APIs (Booking.com Genius rates API exists; Agoda Partner Hub API has rate-export), or (b) a scraper sidecar. Both expensive.
- In-memory storage means a service restart wipes violation history — DPDPA + competitive-intel both need persistence.
Drift findings:
D-013 — [P1] [Stub-data-as-Built + persistence-gap] Story marked Partial, but the deeper issue is the comparison-source: simulated random ±2% diffs, not real OTA rates. Operators get a green dashboard that means nothing.
- [SME-PROPOSED] Add AC: "Parity engine SHALL pull live published rates from at least 2 OTA reporting endpoints (Booking.com Genius API + Agoda Partner Hub) every 4 hours. Violations SHALL persist to
RateParityViolationtable (NOT in-memory) with(propertyId, channelCode, roomTypeId, ratePlanId, date, pmsRate, otaRate, deltaPct, detectedAt, severity, status, exceptionApprovedBy?). Severity:INFO(delta ≤ threshold),WARN(delta > threshold ≤ 5%),CRITICAL(delta > 5%). Default threshold 2% per spec (NOT 5% as today)."
US-009-010 — Channel Performance Report
Catalog status: Partial (P1, 3 points)
Verified status: Partial — basic aggregation by source + ADR; cancellation rate per channel missing; commission data not connected; reporting feeds for analytics-service shipped (/reporting-feeds/channel/sync-log + /rate-parity)
Code locations:
hmp-channel-service/src/stats/*— Stats module: summary, timeline, throughput, alertshmp-channel-service/src/reporting-feeds/*— Live-proxy feeds for analytics-service (Plan 2.2):/reporting-feeds/channel/rate-parity,/reporting-feeds/channel/sync-loghmp-analytics-service/src/*—LiveProxyResolverconsumes channel reporting feeds via INTERNAL_SERVICE_TOKEN
Unit + integration tests: none surfaced from listing (verify Task 6)
Flow tests: none
Real-backend smokes: none
Use cases referencing this story: none
SME notes:
- Stats module + reporting-feeds together cover ~60% of the report spec. Commission tracking is the missing 40% (carries forward to US-009-013).
- Indian-market addition: GST-on-OTA-commission. OTAs charge commission and GST on the commission (Booking.com 18% GST on the commission invoice). Report should show GROSS, NET, and GST-side separately.
- TdsEntry model exists in channel-service (per CLAUDE.md) for OTA-commission TDS tracking — story body silent on TDS requirement (Section 194-O TDS @ 1% on OTA gross).
Drift findings:
D-014 — [P0] [India-tax-critical + story-incomplete] Story is silent on (a) GST-on-OTA-commission (18% reverse charge on Booking.com commission invoice), (b) TDS Section 194-O @ 1% withheld on OTA payouts. Both are LIVE compliance surfaces — incorrect reporting = GSTR-2B mismatch + TDS-default penalty. The TdsEntry model already exists in channel-service schema but is not surfaced in any report.
- [SME-PROPOSED] Add AC: "Channel performance report SHALL present per-channel rows with
grossRevenue,commissionRate,commissionAmount,gstOnCommission(18% reverse-charge on commission, India),tdsWithheld(Section 194-O 1% on gross for OTA payouts),netPayout(=gross - commission - tdsWithheld + gstOnCommission). Exportable to GSTR-2B-ready CSV for the finance team." - [SME-PROPOSED] Add AC: "Channel report SHALL include cancellation rate per channel (cancelled bookings / total bookings) over a configurable window — required to support EP-009 KPI 'Manual intervention rate < 2%' and to evaluate per-channel quality."
US-009-011 — GDS Connectivity (Amadeus/Sabre)
Catalog status: Partial (P2, 8 points)
Verified status: Partial — adapter code exists for Amadeus + Sabre with OAuth2 + ARI push + booking pull; GdsService + GdsController exist; review file flags GdsModule is NOT imported in AppModule → endpoints unreachable dead code
Code locations:
hmp-channel-service/src/gds/gds.controller.ts—GET /channels/gds/providers,/gds/health,POST /gds/push-ari,GET /gds/bookings/:providerhmp-channel-service/src/gds/gds.service.ts— Orchestratorhmp-channel-service/src/adapters/*— Amadeus + Sabre adapter shellshmp-web-dashboard/src/app/(dashboard)/settings/channels/managers/page.tsx— GDS placeholder UI
Unit + integration tests:
hmp-channel-service/src/gds/gds.service.spec.ts
Flow tests: none
Real-backend smokes: none
Use cases referencing this story: none
SME notes:
- Review file 2026-03-07 flags
GdsModulenot imported → dead-code. Must re-verify against currentapp.module.ts. - GDS connectivity in Indian hospitality is only valuable for corporate-travel + airline-crew patterns (Infosys / TCS / Indigo crew). Tier-2/3 hotels often skip it. P2 priority is correct.
- True GDS access requires SiteMinder / RateGain certification ($$$ + months). Mock adapters today buy nothing toward that.
Drift findings: (none net new beyond review file content)
- [SME-PROPOSED] Add AC: "GDS module SHALL be wired into
AppModule.importsand reachable through the API gateway. CI gate: a smoke test callsGET /channels/gds/providersand asserts 200 with at least Amadeus + Sabre listed." - [SME-PROPOSED] Add AC: "GDS adapter activation SHALL gate on
GDS_MODEenv var (mock | sandbox | liveparallel toCHANNEL_MODE). Live requires SiteMinder or RateGain middleware credentials. Activation in QA =mock; production deferred until middleware contract signed."
US-009-012 — Sync Error Monitoring & Retry Queue
Catalog status: Built (P1, 5 points)
Verified status: Built — ChannelSyncLog persistence with status (SUCCESS/FAILED/PARTIAL) + retry count + error message + 2026-05-05 requestXml/responseXml columns; SyncRetryConsumer for retry queue; WebhookEvent audit trail; idempotencyKey (AP-T11-001)
Code locations:
hmp-channel-service/src/sync/sync.service.ts— populatesChannelSyncLogon every push attempt; AP-T11-001 idempotencyKeyhmp-channel-service/src/sync/sync-retry.consumer.ts— Retry queue consumerhmp-channel-service/src/webhooks/webhook-management.service.ts— Replay endpointhmp-channel-service/src/webhooks/intermediary-audit-anomaly-detector.service.ts— AP-T11-024 Phase 4B+4C: 5 anomaly conditions (WHITELIST_BECAME_EMPTY, WHITELIST_LARGE_REMOVAL, WHITELIST_OFF_HOURS, WHITELIST_RAPID_EDITS, BULK_PROVIDER_EDITS)hmp-channel-service/src/webhooks/intermediary-config-notifier.service.ts— Slack notifier (audit + anomaly webhook URLs)hmp-channel-service/src/stats/*— Throughput + alertshmp-web-dashboard/src/components/modules/ari/sync-status-strip.tsx— UI
Unit + integration tests:
hmp-channel-service/src/sync/sync.service.spec.ts(14 tests incl. idempotencyKey shape + retry-stability)hmp-channel-service/src/webhooks/__tests__/intermediary-audit-anomaly-detector.spec.tshmp-channel-service/src/webhooks/__tests__/intermediary-config-notifier.spec.tshmp-channel-service/src/webhooks/__tests__/webhook-management-replay.spec.ts
Flow tests: covered via channel-sync + resavenue-cert-cycle flows
Real-backend smokes: none specific
Use cases referencing this story: none
SME notes:
- This is the strongest story in EP-009 — well-tested, hardened, with anomaly detection + Slack notifier + replay endpoint. Verified-Built is appropriate.
- AP-T11-001 idempotencyKey is the unsung hero — gates duplicate pushes from upstream retries.
- The 2026-05-05 requestXml/responseXml columns closed a real cert-debugging gap.
Drift findings: (none — implementation matches story closely)
US-009-013 — Commission Tracking by Channel
Catalog status: Not Started (P2, 3 points)
Verified status: Not Started — TdsEntry model exists in schema (per CLAUDE.md) but no commission CRUD, no per-OTA rate config, no monthly reconciliation report
Code locations:
hmp-channel-service/prisma/schema.prisma::TdsEntry[CITED-BUT-NOT-FOUND](exact line not enumerated) — TDS tracking model present
Unit + integration tests: NONE FOUND
Flow tests: NONE FOUND
Real-backend smokes: NONE FOUND
Use cases referencing this story: none
SME notes:
- The
TdsEntrymodel is the closest thing to existing scaffolding. Commission rate config (per channel × per room type) does not exist. - Indian-market rates: MakeMyTrip 18-22%, Goibibo 18-22%, Booking.com 15-18%, Expedia 18%, Agoda 17-20%, OYO 22-30%. Plus 18% GST on commission (reverse charge) + 1% TDS Section 194-O.
- This story should fold the GST + TDS surface from D-014 (US-009-010) rather than duplicate it.
Drift findings: (none — story-vs-reality matches: Not Started)
- [SME-PROPOSED] Add AC: "
ChannelCommissionConfigtable:(channelCode, roomTypeId?, commissionPct, effectiveFrom, effectiveTo?). CRUD via/channels/commissions, gatedMANAGER+. Monthly cron computes per-booking commission, persists toBookingCommissionjoin table. Monthly report atGET /reports/commissions?from=&to=with GSTR-2B-ready CSV export (carries D-014)." - [SME-PROPOSED] Add AC: "Cancellation of an OTA booking SHALL reverse the commission entry (write a negative
BookingCommissionrow taggedreversalOfBookingId)."