Trip.com CM Inbound Reservation Push (OpenTravel Notify Model)
Single reference for the inbound reservation endpoint: flow, validation/error
mapping, deployment runbook, credential rotation, simulator usage, and the
Trip.com account-manager handoff (form answers + email draft).
Questionnaire answers Trip.com already has: see docs/ota-api-technical-request.md
(not restated here).
1. Overview
Trip.com (official CM partner, OpenTravel API 4.x) pushes reservation XML
to one endpoint per environment; we process synchronously and answer an
XML RS in the same HTTP exchange (Notify model).
| Env | Endpoint |
|---|---|
| Test | https://cms-st.ptxsolution.com/api/v1/ota/trip/reservations |
| Production | https://cms.ptxsolution.com/api/v1/ota/trip/reservations |
One URL handles all three message types, dispatched by XML root element:
| Root element | Handler | Status |
|---|---|---|
OTA_HotelResRQ | new reservation → Booking ingest | ✅ live |
OTA_HotelResModifyRQ | soft/hard modify (full-payload diff) | ✅ live |
OTA_CancelRQ | stub answering Type=2 | ⛔ blocked — Trip.com has not published the request/response schema; awaiting account manager |
Processing pipeline
POST text/xml
→ PAN structural redaction (parse-then-redact + Luhn sweep) — BEFORE anything else
→ persist raw (redacted) to ota_inbound_messages (durable, persist-first)
→ reject DOCTYPE (XXE/billion-laughs) → RS 10/320
→ parse (fast-xml-parser, processEntities:false)
→ POS auth: RequestorID @ID/@MessagePassword vs TRIP_INBOUND_AUTH_* env
(constant-time HMAC compare, no id-vs-password oracle) → fail RS 4/497
→ resolve OtaAccount (single trip account) — scopes ALL queries (tenant guard)
→ dispatch by root element → handler → XML RS (HTTP 200 always)
→ update ota_inbound_messages outcome/errorCode/processingMsContract: every response is HTTP 200 + XML RS — including guard
exceptions, oversized payloads (>10mb), malformed XML, throttling (disabled on
this route), and internal errors (retryable Type=12 Code=450). Trip.com never
sees the JSON error envelope.
Key code: apps/api/src/modules/ota-inbound/ (controller, TripInboundService,
handlers, validator, response builder, PAN redaction util).
2. Validation & error mapping
Fail-fast pipeline (trip-reservation-validator.service.ts); first failure
answers the error RS. All lookups scoped by the authenticated otaAccountId.
| Check | Pass condition | Error RS (Type/Code) |
|---|---|---|
| Hotel | active OtaConnection for BasicPropertyInfo @HotelCode under the authed account | 3/361 Invalid hotel |
| Hotel linked | OtaConnection.propertyId not null (listing linked to internal Property) | 3/375 Hotel not active |
| Room type | mapping with otaRoomId == RoomTypeCode | 3/402 Invalid room type |
| Rate plan | same mapping has otaRatePlanId == RatePlanCode | 3/249 Invalid rate code |
| Dates parse | TimeSpan Start/End parseable (loose format 2014-3-13 0:00:00); checkIn=min, checkOut=max — spec's own sample has Start/End reversed | 3/15 Invalid date |
| Dates future | checkOut not in the past | 3/353 Departure date is past dated |
| Children | every GuestCount @AgeQualifyingCode="8" has @Age 0–17 | 3/320 Invalid value |
| Amounts | AmountBeforeTax/AmountAfterTax present and > 0 | 3/320 Invalid value |
| Tax math | before/after/tax arithmetic logged only, never rejected (spec examples don't reconcile under any formula — pending sandbox confirmation) | — |
| Currency | 3-letter @CurrencyCode | 3/61 Invalid currency code |
| Reservation id | HotelReservationID @ResID_Type="14" present | 3/315 confirmation number missing |
Other responses:
| Situation | RS |
|---|---|
| Success (new) | <Success/> + HotelReservation ResStatus="S" + UniqueID Type="14" ID="{Booking.internalCode}" |
| Duplicate new push (incl. concurrent retry race — DB unique constraint, P2002 → dedup) | Success + Warning Type="3" Code="127" |
| Duplicate modify (no-diff resend) | Success + Warning Type="3" Code="321" |
| Modify unknown confirmation | 3/245 Invalid confirmation number |
| Bad credentials / missing POS | 4/497 Authentication error |
| Malformed XML / DOCTYPE / wrong content-type | 10/320 Invalid XML message |
| Unsupported root (incl. cancel until schema arrives) | Type=2 No implementation |
| Internal failure (retryable) | 12/450 Unable to process + ops alert/email |
Availability recalc is intentionally out of MVP — bookings persist and
appear in the UI; availability/overbooking wiring is a post-MVP follow-up.
3. Observability & failure handling
- Every inbound message →
ota_inbound_messagesrow (raw_xmlis
PAN-redacted before persist; outcomereceived|success|warning|error,error_code,processing_ms,remote_ip). - Processing failure (12/450 path) additionally fires:
Alert(sync_failure, warning) on the resolved property when the hotel
code is known;- plaintext email to
TRIP_INBOUND_OPS_EMAIL(defaulttien@enti.dev).
Dev/staging delivery goes to Mailpit; production needs real SMTP
(SMTP_HOST/SMTP_PORT/SMTP_USER/SMTP_PASSWORDenv or DB settings) —
without SMTP config the email silently skips (email.service.ts
resolveConfig → null), so verify before go-live.
- PCI: card PANs are structurally redacted (any
CardNumber/PlainText
variant — whitespace/CDATA/namespace-prefix — keeps first 6 + last 4) plus a
defensive Luhn sweep for stray 13–19 digit tokens. Gates: unit tests,
Prisma-backed integration suite
(pan-redaction-persistence.spec.ts), and grep:SELECT count(*) FROM ota_inbound_messages WHERE raw_xml ~ '\d{13,19}'should
return only non-Luhn ids.
4. Credentials
TRIP_INBOUND_AUTH_ID/TRIP_INBOUND_AUTH_PASSWORD— the pair Trip.com
sends inPOS/Source/RequestorID(@ID/@MessagePassword). CM-partner
level (one pair per environment). This is a second credential store,
intentionally separate fromOtaAccount.credentialsEncrypted(outbound
scraping creds).- Generate:
openssl rand -base64 32(each, per environment). Staging ≠ prod. - Rotation: set new values in the Dokploy env for the environment → redeploy →
send new values to Trip.com account manager via secure channel (never
plaintext email). The endpoint compares in constant time; no code change
needed. - Auth model caveat: spec 385 allows CM-partner-level or per-property
credentials,@CodeContextsemantics andType5-vs-1 are unconfirmed —
ask the account manager (email draft below).resolveAccount()intrip-inbound.service.tsis the single seam to swap in per-property lookup.
5. Staging deploy runbook (ptx-cm-staging)
Bản chi tiết step-by-step (UI clicks, env block paste sẵn, verify từng
bước, troubleshooting):docs/runbooks/staging-deploy-cms-st-dokploy.md.
Phần dưới là tóm tắt.
Compose file: docker-compose.staging.yml (separate Dokploy project, own
postgres/redis/mailpit — never share Redis with prod: BullMQ queue names
are bare strings and would cross environments).
External API routing: /api/v1/* and /api/ws/* are routed by Traefik
labels in the compose file DIRECTLY to api:3002. Do not route external/api/v1 calls through the web service — the Next.js catch-all proxy prepends/api/v1 itself and would double the prefix (404).
- Dokploy: create project
ptx-cm-staging→ Compose service pointing at
repomaster, compose pathdocker-compose.staging.yml. - DNS:
cms-st.ptxsolution.com→ A record to the Dokploy server IP
(requires ptxsolution.com zone access). Add the domain to the service in
Dokploy UI → SSL via Let's Encrypt. - Env (Dokploy project env /
.env): copy.env.exampleand set the
FULL list — the API reads many vars at boot. Checklist:PG_PASSWORD,REDIS_PASSWORD— fresh randomsDATABASE_URLis composed in the compose file (dbptx_cm_staging)JWT_SECRET/JWT_*— fresh randomsENCRYPTION_KEY— fresh random; seed ciphertexts are created under this
key by the seed chain, so polling/sync jobs decrypt cleanlyTRIP_INBOUND_AUTH_ID/TRIP_INBOUND_AUTH_PASSWORD— staging pairTRIP_INBOUND_OPS_EMAIL— staging failures go to Mailpit anywayCORS_ORIGIN/FRONTEND_URL→https://cms-st.ptxsolution.comCOUNTRY_SCOPE_ENABLED,CHAT_REALTIME_ENABLED+NEXT_PUBLIC_*build
args — mirror prod values
- Deploy. The api entrypoint applies migrations (
prisma migrate deploy) — data persists across deploys. On an empty database (first
boot) it also runs the fullseed.tschain (includesseed-booking-workflow→BookingStatusDef, org hierarchy, internal
codes).DB_FORCE_RESET=trueenv = one-time wipe-and-reseed escape hatch
(also the migration path off schemas created by old db-push deploys) —
remove the variable after that deploy. - Simulator test data: from a checkout (or one-off container):
npx dotenv-cli -e .env -- npx tsx packages/database/prisma/seed-trip-inbound-test-data.ts
→ prints the hotel/room/rate codes and runs the validator acceptance check. - Self-cert:bashExpect 10/10 PASS (cancel case intentionally absent until schema arrives).
TRIP_INBOUND_AUTH_ID=... TRIP_INBOUND_AUTH_PASSWORD=... \ npx tsx scripts/trip-simulator/send-trip-inbound-message.ts \ --url https://cms-st.ptxsolution.com/api/v1/ota/trip/reservations --all - Verify isolation: staging Redis has no prod queue keys; staging DB has
no prod rows; UI bookings page renders statuses (BookingStatusDef seeded).
Production
No new infra — the route ships with the api at cms.ptxsolution.com:
- Set
TRIP_INBOUND_AUTH_ID/TRIP_INBOUND_AUTH_PASSWORD(+TRIP_INBOUND_OPS_EMAIL,
realSMTP_*) in the prod Dokploy env. Prod pair ≠ staging pair. - Deploy the release.
- Smoke (no data needed): POST any XML with wrong creds → expect
Errors/Error @Type="4" @Code="497", HTTP 200,Content-Type: text/xml.
6. Simulator (self-certification tool)
scripts/trip-simulator/ — plays Trip.com against any environment (HTTP only,
no DB access). Fixtures are placeholder-driven (,, dates auto-set to future) — no credentials in the repo.
# all 10 cases
npx tsx scripts/trip-simulator/send-trip-inbound-message.ts --all
# one case, other URL
npx tsx scripts/trip-simulator/send-trip-inbound-message.ts \
--case resend-duplicate-new --url https://cms-st.ptxsolution.com/api/v1/ota/trip/reservationsCovers: 4 payment variants (guest card / VCC / hotel-collect / trip-collect),
soft+hard modify, duplicate new (3/127), duplicate modify (3/321), auth failure
(4/497), unknown root (Type=2). Cancel fixture is added once Trip.com provides
the OTA_CancelRQ schema. Per-error-code validation cases live in unit tests
(trip-reservation-validator.service.spec.ts), not XML fixtures.
7. Trip.com handoff — form answers
Test reservation endpoint:
https://cms-st.ptxsolution.com/api/v1/ota/trip/reservations
(Single endpoint for OTA_HotelResRQ / OTA_HotelResModifyRQ / OTA_CancelRQ,
dispatched by message root element. Credentials shared via secure channel.)
Production reservation endpoint:
https://cms.ptxsolution.com/api/v1/ota/trip/reservations
Notification email (activation/deactivation): tien@enti.dev8. Trip.com handoff — account manager email draft
Subject: PTX CM reservation push endpoints ready — certification + open questions
Hi {account manager},
Our reservation push (Notify model) endpoints are ready:
- Test:
https://cms-st.ptxsolution.com/api/v1/ota/trip/reservations- Production:
https://cms.ptxsolution.com/api/v1/ota/trip/reservations- One endpoint per environment handles OTA_HotelResRQ / OTA_HotelResModifyRQ /
OTA_CancelRQ, dispatched by root element — please confirm a single shared
URL for modify/cancel is acceptable.- Notification email for activation/deactivation: tien@enti.dev
- POS credentials will be shared via a secure channel (please indicate your
preferred mechanism — we will not send passwords by email).To schedule certification we still need from your side:
- Test hotel codes plus the RoomTypeCode / RatePlanCode values mapped to
them for the certification environment.- Certification schedule — earliest available slot.
- OTA_CancelRQ / OTA_CancelRS schema + sandbox access. The public doc
page for the Cancellation API has no request/response examples or field
tables; we have intentionally not guessed the shape. Cancel currently
answers "no implementation" until we receive the schema.- AES key for
TPA_Extensions/ActualPhoneNumberso we can decrypt the
guest's actual phone number (we currently store the ciphertext).- Authentication model confirmation: is the RequestorID
@ID/@MessagePasswordpair CM-partner-level or per-property? What@CodeContextvalue should we expect, and is@Type5 (push) vs 1
(retrieval) significant for validation?- Duplicate-cancel warning code — the resend warning code for an
already-cancelled reservation isn't documented (new=127, modify=321).Thanks,
9. Open items / follow-ups
| Item | Owner | Blocking |
|---|---|---|
DNS cms-st.ptxsolution.com (zone access) | user | staging deploy |
Dokploy ptx-cm-staging project + env | user | staging deploy |
Prod env TRIP_INBOUND_AUTH_* + SMTP + deploy | user | prod go-live |
Mailbox tien@enti.dev receives external mail | user | handoff |
| OTA_CancelRQ schema + sandbox | Trip.com AM | cancel handler + fixture |
| Test hotel codes + cert schedule | Trip.com AM | certification |
| Auth model / @CodeContext / Type 5-vs-1 | Trip.com AM | cert (soft) |
| AES key for ActualPhoneNumber | Trip.com AM | follow-up, non-blocking |
| Availability recalc wiring for OTA push | dev | post-MVP follow-up |