Skip to content

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).

EnvEndpoint
Testhttps://cms-st.ptxsolution.com/api/v1/ota/trip/reservations
Productionhttps://cms.ptxsolution.com/api/v1/ota/trip/reservations

One URL handles all three message types, dispatched by XML root element:

Root elementHandlerStatus
OTA_HotelResRQnew reservation → Booking ingest✅ live
OTA_HotelResModifyRQsoft/hard modify (full-payload diff)✅ live
OTA_CancelRQstub 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/processingMs

Contract: 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.

CheckPass conditionError RS (Type/Code)
Hotelactive OtaConnection for BasicPropertyInfo @HotelCode under the authed account3/361 Invalid hotel
Hotel linkedOtaConnection.propertyId not null (listing linked to internal Property)3/375 Hotel not active
Room typemapping with otaRoomId == RoomTypeCode3/402 Invalid room type
Rate plansame mapping has otaRatePlanId == RatePlanCode3/249 Invalid rate code
Dates parseTimeSpan Start/End parseable (loose format 2014-3-13 0:00:00); checkIn=min, checkOut=max — spec's own sample has Start/End reversed3/15 Invalid date
Dates futurecheckOut not in the past3/353 Departure date is past dated
Childrenevery GuestCount @AgeQualifyingCode="8" has @Age 0–173/320 Invalid value
AmountsAmountBeforeTax/AmountAfterTax present and > 03/320 Invalid value
Tax mathbefore/after/tax arithmetic logged only, never rejected (spec examples don't reconcile under any formula — pending sandbox confirmation)
Currency3-letter @CurrencyCode3/61 Invalid currency code
Reservation idHotelReservationID @ResID_Type="14" present3/315 confirmation number missing

Other responses:

SituationRS
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 confirmation3/245 Invalid confirmation number
Bad credentials / missing POS4/497 Authentication error
Malformed XML / DOCTYPE / wrong content-type10/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_messages row (raw_xml is
    PAN-redacted before persist; outcome received|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 (default tien@enti.dev).
      Dev/staging delivery goes to Mailpit; production needs real SMTP
      (SMTP_HOST/SMTP_PORT/SMTP_USER/SMTP_PASSWORD env 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 in POS/Source/RequestorID (@ID / @MessagePassword). CM-partner
    level (one pair per environment). This is a second credential store,
    intentionally separate from OtaAccount.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, @CodeContext semantics and Type 5-vs-1 are unconfirmed —
    ask the account manager (email draft below). resolveAccount() in
    trip-inbound.service.ts is 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).

  1. Dokploy: create project ptx-cm-staging → Compose service pointing at
    repo master, compose path docker-compose.staging.yml.
  2. 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.
  3. Env (Dokploy project env / .env): copy .env.example and set the
    FULL list — the API reads many vars at boot. Checklist:
    • PG_PASSWORD, REDIS_PASSWORD — fresh randoms
    • DATABASE_URL is composed in the compose file (db ptx_cm_staging)
    • JWT_SECRET/JWT_* — fresh randoms
    • ENCRYPTION_KEY — fresh random; seed ciphertexts are created under this
      key by the seed chain, so polling/sync jobs decrypt cleanly
    • TRIP_INBOUND_AUTH_ID / TRIP_INBOUND_AUTH_PASSWORD — staging pair
    • TRIP_INBOUND_OPS_EMAIL — staging failures go to Mailpit anyway
    • CORS_ORIGIN / FRONTEND_URLhttps://cms-st.ptxsolution.com
    • COUNTRY_SCOPE_ENABLED, CHAT_REALTIME_ENABLED + NEXT_PUBLIC_* build
      args — mirror prod values
  4. Deploy. The api entrypoint applies migrations (prisma migrate deploy) — data persists across deploys. On an empty database (first
    boot) it also runs the full seed.ts chain (includes
    seed-booking-workflowBookingStatusDef, org hierarchy, internal
    codes). DB_FORCE_RESET=true env = 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.
  5. 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.
  6. Self-cert:
    bash
    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
    Expect 10/10 PASS (cancel case intentionally absent until schema arrives).
  7. 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:

  1. Set TRIP_INBOUND_AUTH_ID/TRIP_INBOUND_AUTH_PASSWORD (+TRIP_INBOUND_OPS_EMAIL,
    real SMTP_*) in the prod Dokploy env. Prod pair ≠ staging pair.
  2. Deploy the release.
  3. 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.

bash
# 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/reservations

Covers: 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.dev

8. 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:

  1. Test hotel codes plus the RoomTypeCode / RatePlanCode values mapped to
    them for the certification environment.
  2. Certification schedule — earliest available slot.
  3. 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.
  4. AES key for TPA_Extensions/ActualPhoneNumber so we can decrypt the
    guest's actual phone number (we currently store the ciphertext).
  5. Authentication model confirmation: is the RequestorID @ID /
    @MessagePassword pair CM-partner-level or per-property? What
    @CodeContext value should we expect, and is @Type 5 (push) vs 1
    (retrieval) significant for validation?
  6. 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

ItemOwnerBlocking
DNS cms-st.ptxsolution.com (zone access)userstaging deploy
Dokploy ptx-cm-staging project + envuserstaging deploy
Prod env TRIP_INBOUND_AUTH_* + SMTP + deployuserprod go-live
Mailbox tien@enti.dev receives external mailuserhandoff
OTA_CancelRQ schema + sandboxTrip.com AMcancel handler + fixture
Test hotel codes + cert scheduleTrip.com AMcertification
Auth model / @CodeContext / Type 5-vs-1Trip.com AMcert (soft)
AES key for ActualPhoneNumberTrip.com AMfollow-up, non-blocking
Availability recalc wiring for OTA pushdevpost-MVP follow-up

PTX Channel Manager — Tài Liệu Nội Bộ