From c6f567028c5b8f3febcbd15c0d84c09a6b59f130 Mon Sep 17 00:00:00 2001 From: Flamur Maliqi Date: Wed, 19 Aug 2026 20:06:32 +0200 Subject: [PATCH 1/2] refactor(api): remove checkout command compatibility --- apps/api/.env.example | 6 +- apps/api/src/app.test.ts | 26 ++++ apps/api/src/app.ts | 6 +- apps/api/src/config.test.ts | 46 +------ apps/api/src/config.ts | 43 +----- apps/api/src/nextApiLegacyFree.test.ts | 10 +- apps/api/src/routes/bookingWebPublic.ts | 58 +------- .../src/routes/bookingWebPublicParity.test.ts | 130 ++---------------- apps/api/src/server.ts | 50 +++---- ...pps-api-legacy-runtime-dependency-audit.md | 30 ++-- .../booking-pms-route-migration-inventory.md | 26 ++-- engineering/booking-web-public-api-routing.md | 41 +++--- .../next-stack-legacy-dependency-inventory.md | 3 +- scripts/dev-workos-local.sh | 1 - 14 files changed, 135 insertions(+), 341 deletions(-) diff --git a/apps/api/.env.example b/apps/api/.env.example index 46816ccbda..3d49b248cc 100644 --- a/apps/api/.env.example +++ b/apps/api/.env.example @@ -40,7 +40,6 @@ PORT=8003 # PUBLIC_HOTEL_PROFILE_SOURCE=active_publication # PMS_OPERATIONS_SOURCE=target # FINANCE_SOURCE=target -# BOOKING_CHECKOUT_COMMAND_SOURCE=target # # For next-stack surfaces that are not available yet, set their source # explicitly to disabled instead of relying on the local disabled default. @@ -64,18 +63,17 @@ PORT=8003 # Fixed Plan subscriptions (VAY-1120) # -# Finance and Booking checkout must both use the target database. Stripe Checkout +# Finance and Booking checkout both use the target database. Stripe Checkout # activates Fixed only after an `invoice.paid` webhook; recurring failures keep # the plan active while Stripe retries. The fixed Price is created once with a # 30-day interval unless a pre-provisioned Price ID is supplied. # FINANCE_SOURCE=target -# BOOKING_CHECKOUT_COMMAND_SOURCE=target # STRIPE_SECRET_KEY=sk_... # STRIPE_FIXED_PLAN_PRICE_ID=price_... # optional # STRIPE_WEBHOOK_SECRET=whsec_... # STRIPE_WEBHOOK_INTAKE_MODE=mutating # BOOKING_ADMIN_BASE_URL=https://admin.booking.localhost -# RESEND_API_KEY=re_... # required for target Booking checkout in production +# RESEND_API_KEY=re_... # required for Booking checkout in production # BOOKING_EMAIL_FROM=Vayada Bookings # Platform media serving (VAY-821) diff --git a/apps/api/src/app.test.ts b/apps/api/src/app.test.ts index 889d649ff9..ec7ea570bb 100644 --- a/apps/api/src/app.test.ts +++ b/apps/api/src/app.test.ts @@ -82,6 +82,7 @@ import { import { createTargetBookingWebCalendarRepository, type BookingWebCalendarReadPool, + type BookingWebCheckoutAdapter, type BookingHotelChangeRequestRepository, } from "./routes/bookingWebPublic.js"; import type { @@ -2389,6 +2390,8 @@ const publicHotelProfileRepository: PublicHotelProfileRepository = { }, }; +const unusedBookingWebCheckoutAdapter = {} as BookingWebCheckoutAdapter; + function targetPublicHotelProfileRow(): QueryResultRow { return { propertyId: "f6893000-0000-0000-0000-000000000001", @@ -2944,6 +2947,7 @@ describe("vayada-api", () => { app = buildApp({ logger: false, publicHotelProfileRepository, + bookingWebCheckoutAdapter: unusedBookingWebCheckoutAdapter, }); const response = await app.inject({ @@ -3027,6 +3031,15 @@ describe("vayada-api", () => { expect(findForbiddenPublicBookabilityKeys(body)).toEqual([]); }); + it("requires the target checkout adapter when Booking Web public routes are mounted", () => { + expect(() => + buildApp({ + logger: false, + publicHotelProfileRepository, + }), + ).toThrow("Booking Web checkout adapter is required when public routes are mounted"); + }); + it("returns stable unavailable reason codes for public AI hotel quotes", async () => { app = buildApp({ logger: false, @@ -3054,6 +3067,7 @@ describe("vayada-api", () => { logger: false, publicHotelProfileRepository, publicHotelQuoteRepository, + bookingWebCheckoutAdapter: unusedBookingWebCheckoutAdapter, }); const response = await app.inject({ @@ -3091,6 +3105,7 @@ describe("vayada-api", () => { logger: false, publicHotelProfileRepository, publicHotelQuoteRepository, + bookingWebCheckoutAdapter: unusedBookingWebCheckoutAdapter, }); const response = await app.inject({ @@ -3131,6 +3146,7 @@ describe("vayada-api", () => { }, }, publicHotelQuoteRepository, + bookingWebCheckoutAdapter: unusedBookingWebCheckoutAdapter, }); const response = await injectJson(app, { @@ -3170,6 +3186,7 @@ describe("vayada-api", () => { }, }, publicHotelQuoteRepository, + bookingWebCheckoutAdapter: unusedBookingWebCheckoutAdapter, }); const response = await injectJson(app, { @@ -3208,6 +3225,7 @@ describe("vayada-api", () => { app = buildApp({ logger: false, publicHotelProfileRepository: targetRepository, + bookingWebCheckoutAdapter: unusedBookingWebCheckoutAdapter, }); const aiProfile = await injectJson(app, { @@ -3251,6 +3269,7 @@ describe("vayada-api", () => { }, }, publicHotelQuoteRepository, + bookingWebCheckoutAdapter: unusedBookingWebCheckoutAdapter, }); const response = await injectJson(app, { @@ -3266,6 +3285,7 @@ describe("vayada-api", () => { logger: false, publicHotelProfileRepository, publicHotelQuoteRepository, + bookingWebCheckoutAdapter: unusedBookingWebCheckoutAdapter, }); const response = await app.inject({ @@ -3297,6 +3317,7 @@ describe("vayada-api", () => { logger: false, publicHotelProfileRepository, publicHotelQuoteRepository, + bookingWebCheckoutAdapter: unusedBookingWebCheckoutAdapter, }); const preflight = await app.inject({ @@ -3340,6 +3361,7 @@ describe("vayada-api", () => { logger: false, publicHotelProfileRepository, publicHotelQuoteRepository, + bookingWebCheckoutAdapter: unusedBookingWebCheckoutAdapter, }); const preflight = await app.inject({ @@ -3373,6 +3395,7 @@ describe("vayada-api", () => { logger: false, publicHotelProfileRepository, publicHotelQuoteRepository, + bookingWebCheckoutAdapter: unusedBookingWebCheckoutAdapter, }); const response = await app.inject({ @@ -3414,6 +3437,7 @@ describe("vayada-api", () => { logger: false, publicHotelProfileRepository, publicHotelQuoteRepository, + bookingWebCheckoutAdapter: unusedBookingWebCheckoutAdapter, bookingWebPublicNow: () => new Date("2026-06-06T11:00:00.000Z"), }); @@ -3474,6 +3498,7 @@ describe("vayada-api", () => { logger: false, publicHotelProfileRepository, publicHotelQuoteRepository, + bookingWebCheckoutAdapter: unusedBookingWebCheckoutAdapter, bookingWebCalendarRepository: { async findCalendarByHotel(hotel, query) { return { @@ -3595,6 +3620,7 @@ describe("vayada-api", () => { return pollutedProfile; }, }, + bookingWebCheckoutAdapter: unusedBookingWebCheckoutAdapter, }); const response = await app.inject({ diff --git a/apps/api/src/app.ts b/apps/api/src/app.ts index f415dbdbb8..38495bf12b 100644 --- a/apps/api/src/app.ts +++ b/apps/api/src/app.ts @@ -384,12 +384,16 @@ export function buildApp(options: BuildAppOptions = {}): FastifyInstance { }); } if (options.publicHotelProfileRepository) { + const bookingWebCheckoutAdapter = options.bookingWebCheckoutAdapter; + if (!bookingWebCheckoutAdapter) { + throw new Error("Booking Web checkout adapter is required when public routes are mounted"); + } app.register(registerBookingWebPublicRoutes, { prefix: "/api/booking-web", profileRepository: options.publicHotelProfileRepository, quoteRepository: options.publicHotelQuoteRepository, calendarRepository: options.bookingWebCalendarRepository, - checkoutAdapter: options.bookingWebCheckoutAdapter, + checkoutAdapter: bookingWebCheckoutAdapter, affiliateHotelResolver: options.bookingWebAffiliateHotelResolver ?? options.publicHotelProfileRepository, affiliateRepository: options.bookingWebAffiliateRepository, diff --git a/apps/api/src/config.test.ts b/apps/api/src/config.test.ts index 69d2405599..c3184e09e5 100644 --- a/apps/api/src/config.test.ts +++ b/apps/api/src/config.test.ts @@ -329,7 +329,6 @@ describe("api config", () => { PUBLIC_HOTEL_PROFILE_SOURCE: "target", PMS_OPERATIONS_SOURCE: "target", FINANCE_SOURCE: "target", - BOOKING_CHECKOUT_COMMAND_SOURCE: "target", }); expect(config).toMatchObject({ @@ -337,7 +336,6 @@ describe("api config", () => { publicHotelProfileSource: "target", pmsOperationsSource: "target", financeSource: "target", - bookingCheckoutCommandSource: "target", }); }); @@ -348,7 +346,6 @@ describe("api config", () => { PUBLIC_HOTEL_PROFILE_SOURCE: "target", PMS_OPERATIONS_SOURCE: "disabled", FINANCE_SOURCE: "target", - BOOKING_CHECKOUT_COMMAND_SOURCE: "target", }); expect(config.pmsOperationsSource).toBe("disabled"); @@ -373,7 +370,7 @@ describe("api config", () => { TARGET_DATABASE_URL: "postgresql://target-db", }), ).toThrow( - "API_RUNTIME=next requires target runtime sources: PMS_OPERATIONS_SOURCE=target or explicit disabled, FINANCE_SOURCE=target, BOOKING_CHECKOUT_COMMAND_SOURCE=target", + "API_RUNTIME=next requires target runtime sources: PMS_OPERATIONS_SOURCE=target or explicit disabled, FINANCE_SOURCE=target", ); }); @@ -436,7 +433,6 @@ describe("api config", () => { const stripeRuntimeEnv = { TARGET_DATABASE_URL: "postgresql://target-db", FINANCE_SOURCE: "target", - BOOKING_CHECKOUT_COMMAND_SOURCE: "target", STRIPE_SECRET_KEY: "sk_test_subscription", STRIPE_FIXED_PLAN_PRICE_ID: "price_fixed", STRIPE_WEBHOOK_SECRET: "whsec_subscription", @@ -454,7 +450,6 @@ describe("api config", () => { { ...stripeRuntimeEnv, STRIPE_SECRET_KEY: undefined }, { ...stripeRuntimeEnv, STRIPE_WEBHOOK_SECRET: undefined }, { ...stripeRuntimeEnv, STRIPE_WEBHOOK_INTAKE_MODE: "observe_only" }, - { ...stripeRuntimeEnv, BOOKING_CHECKOUT_COMMAND_SOURCE: "legacy_proxy" }, ]) { expect(stripeSubscriptionRuntimeEnabled(loadConfig(env))).toBe(false); } @@ -658,26 +653,11 @@ describe("api config", () => { ).toEqual(["https://marketplace.localhost", "https://admin.localhost"]); }); - it("keeps Booking Web checkout commands on the legacy proxy source by default", () => { - expect(loadConfig({}).bookingCheckoutCommandSource).toBe("legacy_proxy"); - }); - - it("loads target Booking Web checkout command source config", () => { - const config = loadConfig({ - TARGET_DATABASE_URL: "postgresql://target-db", - BOOKING_CHECKOUT_COMMAND_SOURCE: "target", - }); - - expect(config.bookingCheckoutCommandSource).toBe("target"); - expect(config.targetDatabaseUrl).toBe("postgresql://target-db"); - }); - - it("requires booking email delivery for target checkout in production", () => { + it("requires booking email delivery for checkout in production", () => { expect(() => loadConfig({ NODE_ENV: "production", TARGET_DATABASE_URL: "postgresql://target-db", - BOOKING_CHECKOUT_COMMAND_SOURCE: "target", }), ).toThrow("requires RESEND_API_KEY and BOOKING_EMAIL_FROM"); @@ -685,7 +665,6 @@ describe("api config", () => { loadConfig({ NODE_ENV: "production", TARGET_DATABASE_URL: "postgresql://target-db", - BOOKING_CHECKOUT_COMMAND_SOURCE: "target", RESEND_API_KEY: "re_test", BOOKING_EMAIL_FROM: "Vayada ", FINANCE_SOURCE: "target", @@ -700,11 +679,10 @@ describe("api config", () => { }); }); - it("requires the Stripe mutation and recovery runtime for target checkout in production", () => { + it("requires the Stripe mutation and recovery runtime for checkout in production", () => { const complete = { NODE_ENV: "production", TARGET_DATABASE_URL: "postgresql://target-db", - BOOKING_CHECKOUT_COMMAND_SOURCE: "target", FINANCE_SOURCE: "target", RESEND_API_KEY: "re_test", BOOKING_EMAIL_FROM: "Vayada ", @@ -712,7 +690,7 @@ describe("api config", () => { STRIPE_WEBHOOK_SECRET: "whsec_test", STRIPE_WEBHOOK_INTAKE_MODE: "mutating", }; - expect(loadConfig(complete).bookingCheckoutCommandSource).toBe("target"); + expect(stripeSubscriptionRuntimeEnabled(loadConfig(complete))).toBe(true); for (const env of [ { ...complete, STRIPE_SECRET_KEY: undefined }, { ...complete, STRIPE_WEBHOOK_SECRET: undefined }, @@ -723,22 +701,6 @@ describe("api config", () => { } }); - it("requires target database config for target Booking Web checkout commands", () => { - expect(() => - loadConfig({ - BOOKING_CHECKOUT_COMMAND_SOURCE: "target", - }), - ).toThrow("BOOKING_CHECKOUT_COMMAND_SOURCE=target requires TARGET_DATABASE_URL"); - }); - - it("rejects unsupported Booking Web checkout command source config", () => { - expect(() => - loadConfig({ - BOOKING_CHECKOUT_COMMAND_SOURCE: "preview", - }), - ).toThrow("BOOKING_CHECKOUT_COMMAND_SOURCE must be one of: legacy_proxy, target"); - }); - it("loads optional booking host base config", () => { expect( loadConfig({ diff --git a/apps/api/src/config.ts b/apps/api/src/config.ts index d18da9f310..bacdb8a611 100644 --- a/apps/api/src/config.ts +++ b/apps/api/src/config.ts @@ -45,7 +45,6 @@ export type ApiAuthSessionConfig = { export type PublicHotelProfileSource = "target" | "active_publication"; export type MarketplaceAdminSource = "disabled" | "target"; -export type BookingCheckoutCommandSource = "legacy_proxy" | "target"; export type PmsOperationsSource = "disabled" | "target"; export type FinanceSource = "legacy" | "target"; export type BookingWebEventSink = "disabled" | "target"; @@ -90,14 +89,10 @@ export type BookingEmailDeliveryConfig = { }; export function stripeSubscriptionRuntimeEnabled( - config: Pick< - ApiConfig, - "bookingCheckoutCommandSource" | "financeSource" | "providerWebhooks" | "stripeSubscriptions" - >, + config: Pick, ): boolean { return ( config.financeSource === "target" && - config.bookingCheckoutCommandSource === "target" && Boolean(config.stripeSubscriptions.secretKey) && Boolean(config.providerWebhooks.stripeSecret) && config.providerWebhooks.stripeMode === "mutating" @@ -145,7 +140,6 @@ export type ApiConfig = { marketplaceDiscoveryAllowedOrigins: string[]; affiliatePublicSource?: "target"; pmsOperationsAllowedOrigins: string[]; - bookingCheckoutCommandSource: BookingCheckoutCommandSource; bookingWebEventSink: BookingWebEventSink; bookingHostBase?: string; platformMediaServing?: PlatformMediaServingConfig; @@ -644,12 +638,6 @@ export function loadConfig(env: NodeJS.ProcessEnv = process.env): ApiConfig { ["disabled", "target"], "disabled", ); - const bookingCheckoutCommandSource = readSourceEnv( - env, - "BOOKING_CHECKOUT_COMMAND_SOURCE", - ["legacy_proxy", "target"], - "legacy_proxy", - ); const pmsOperationsSource = readSourceEnv( env, "PMS_OPERATIONS_SOURCE", @@ -675,7 +663,6 @@ export function loadConfig(env: NodeJS.ProcessEnv = process.env): ApiConfig { publicHotelProfileSource, pmsOperationsSource, financeSource, - bookingCheckoutCommandSource, }); if (marketplaceAdminSource === "target" && !targetDatabaseUrl) { throw new Error("TARGET_DATABASE_URL is required when MARKETPLACE_ADMIN_SOURCE=target"); @@ -692,9 +679,6 @@ export function loadConfig(env: NodeJS.ProcessEnv = process.env): ApiConfig { if (bookingWebEventSink === "target" && !auth) { throw new Error("BOOKING_WEB_EVENT_SINK=target requires complete auth config"); } - if (bookingCheckoutCommandSource === "target" && !targetDatabaseUrl) { - throw new Error("BOOKING_CHECKOUT_COMMAND_SOURCE=target requires TARGET_DATABASE_URL"); - } if (targetDatabaseUrl && auth && !platformMediaServing) { throw new Error( "Target Marketplace with complete auth requires complete PLATFORM_MEDIA_* config because creator profile photos are required", @@ -705,18 +689,13 @@ export function loadConfig(env: NodeJS.ProcessEnv = process.env): ApiConfig { "Creator platform connections require TARGET_DATABASE_URL and complete auth config", ); } - if ( - env.NODE_ENV === "production" && - bookingCheckoutCommandSource === "target" && - !bookingEmailDelivery - ) { + if (env.NODE_ENV === "production" && !bookingEmailDelivery) { throw new Error( - "BOOKING_CHECKOUT_COMMAND_SOURCE=target requires RESEND_API_KEY and BOOKING_EMAIL_FROM in production", + "Target booking checkout requires RESEND_API_KEY and BOOKING_EMAIL_FROM in production", ); } const prospectiveConfig = { financeSource, - bookingCheckoutCommandSource, stripeSubscriptions: loadStripeSubscriptionConfig(env), providerWebhooks: loadProviderWebhookConfig(env), }; @@ -727,13 +706,9 @@ export function loadConfig(env: NodeJS.ProcessEnv = process.env): ApiConfig { ) { throw new Error("Mutating PMS Channex capabilities require PMS_OPERATIONS_SOURCE=target"); } - if ( - env.NODE_ENV === "production" && - bookingCheckoutCommandSource === "target" && - !stripeSubscriptionRuntimeEnabled(prospectiveConfig) - ) { + if (env.NODE_ENV === "production" && !stripeSubscriptionRuntimeEnabled(prospectiveConfig)) { throw new Error( - "BOOKING_CHECKOUT_COMMAND_SOURCE=target requires STRIPE_SECRET_KEY, STRIPE_WEBHOOK_SECRET, STRIPE_WEBHOOK_INTAKE_MODE=mutating, and FINANCE_SOURCE=target in production", + "Target booking checkout requires STRIPE_SECRET_KEY, STRIPE_WEBHOOK_SECRET, STRIPE_WEBHOOK_INTAKE_MODE=mutating, and FINANCE_SOURCE=target in production", ); } @@ -761,7 +736,6 @@ export function loadConfig(env: NodeJS.ProcessEnv = process.env): ApiConfig { "https://admin.booking.localhost", "https://marketplace.localhost", ]), - bookingCheckoutCommandSource, bookingWebEventSink, bookingHostBase: readOptionalEnv(env, "BOOKING_HOST_BASE"), platformMediaServing, @@ -820,11 +794,7 @@ function assertNextApiRuntimeConfig( env: NodeJS.ProcessEnv, config: Pick< ApiConfig, - | "apiRuntime" - | "publicHotelProfileSource" - | "pmsOperationsSource" - | "financeSource" - | "bookingCheckoutCommandSource" + "apiRuntime" | "publicHotelProfileSource" | "pmsOperationsSource" | "financeSource" >, ): void { if (config.apiRuntime !== "next") return; @@ -841,7 +811,6 @@ function assertNextApiRuntimeConfig( allowExplicitDisabled: true, }, { key: "FINANCE_SOURCE", value: config.financeSource }, - { key: "BOOKING_CHECKOUT_COMMAND_SOURCE", value: config.bookingCheckoutCommandSource }, ].flatMap((source) => nextRuntimeSourceRequirements(env, source)); if (requiredTargetSources.length > 0) { diff --git a/apps/api/src/nextApiLegacyFree.test.ts b/apps/api/src/nextApiLegacyFree.test.ts index c9eb890b70..3c0e4b2940 100644 --- a/apps/api/src/nextApiLegacyFree.test.ts +++ b/apps/api/src/nextApiLegacyFree.test.ts @@ -10,7 +10,10 @@ import { loadConfig } from "./config.js"; import { createPublicRuntimeRepositories } from "./publicRuntime.js"; import type { PublicHotelQuoteReadPool } from "./routes/aiHotelQuotes.js"; import type { PublicHotelProfileReadPool } from "./routes/aiHotels.js"; -import type { BookingWebCalendarReadPool } from "./routes/bookingWebPublic.js"; +import type { + BookingWebCalendarReadPool, + BookingWebCheckoutAdapter, +} from "./routes/bookingWebPublic.js"; import type { MarketplaceDiscoveryReadPool } from "./routes/marketplaceDiscovery.js"; const legacyRuntimeEnvKeys = [ @@ -20,13 +23,14 @@ const legacyRuntimeEnvKeys = [ "MARKETPLACE_DATABASE_URL", ] as const; +const unusedBookingWebCheckoutAdapter = {} as BookingWebCheckoutAdapter; + const nextApiLegacyFreeEnv: NodeJS.ProcessEnv = { API_RUNTIME: "next", TARGET_DATABASE_URL: "postgresql://target-db", PUBLIC_HOTEL_PROFILE_SOURCE: "active_publication", PMS_OPERATIONS_SOURCE: "target", FINANCE_SOURCE: "target", - BOOKING_CHECKOUT_COMMAND_SOURCE: "target", }; const publicBookabilityFixture = PUBLIC_BOOKABILITY_FIXTURES.find( @@ -118,7 +122,6 @@ describe("next-api legacy-free runtime check", () => { publicHotelProfileSource: "active_publication", pmsOperationsSource: "target", financeSource: "target", - bookingCheckoutCommandSource: "target", }); const publicRuntime = createPublicRuntimeRepositories(config, { @@ -131,6 +134,7 @@ describe("next-api legacy-free runtime check", () => { app = buildApp({ logger: false, ...publicRuntime, + bookingWebCheckoutAdapter: unusedBookingWebCheckoutAdapter, }); await app.ready(); diff --git a/apps/api/src/routes/bookingWebPublic.ts b/apps/api/src/routes/bookingWebPublic.ts index 9dbfcb18eb..7e9c7c5547 100644 --- a/apps/api/src/routes/bookingWebPublic.ts +++ b/apps/api/src/routes/bookingWebPublic.ts @@ -413,7 +413,7 @@ export type BookingWebPublicRoutesOptions = { profileRepository: PublicHotelProfileRepository; quoteRepository?: PublicHotelQuoteRepository; calendarRepository?: BookingWebCalendarRepository; - checkoutAdapter?: BookingWebCheckoutAdapter; + checkoutAdapter: BookingWebCheckoutAdapter; affiliateHotelResolver?: BookingWebAffiliateHotelResolver; affiliateRepository?: BookingWebAffiliateRepository; affiliateAdapter?: BookingWebAffiliateAdapter; @@ -426,7 +426,7 @@ export async function registerBookingWebPublicRoutes( options: BookingWebPublicRoutesOptions, ): Promise { const now = options.now ?? (() => new Date()); - const checkoutAdapter = options.checkoutAdapter ?? createUnavailableBookingWebCheckoutAdapter(); + const checkoutAdapter = options.checkoutAdapter; const affiliateAdapter = options.affiliateAdapter ?? createUnavailableBookingWebAffiliateAdapter(); @@ -2296,60 +2296,6 @@ function assertTargetBookingConfirmationTokenActive( } } -export function createUnavailableBookingWebCheckoutAdapter(): BookingWebCheckoutAdapter { - return { - async consumeLookupAttempt() { - throw createHttpError(404, "Booking Web checkout adapter is not configured."); - }, - async getCheckoutConfig() { - throw createHttpError(404, "Booking Web checkout adapter is not configured."); - }, - async createBooking() { - throw createHttpError(404, "Booking Web checkout command adapter is not configured."); - }, - async quoteBooking() { - throw createHttpError(404, "Booking Web checkout command adapter is not configured."); - }, - async confirmAuthorization() { - throw createHttpError(404, "Booking Web checkout command adapter is not configured."); - }, - async getStatus() { - throw createHttpError(404, "Booking Web checkout adapter is not configured."); - }, - async lookup() { - throw createHttpError(404, "Booking Web checkout adapter is not configured."); - }, - async withdraw() { - throw createHttpError(404, "Booking Web checkout command adapter is not configured."); - }, - async cancelPreview() { - throw createHttpError(404, "Booking Web checkout command adapter is not configured."); - }, - async cancel() { - throw createHttpError(404, "Booking Web checkout command adapter is not configured."); - }, - async previewChangeRequest() { - throw createHttpError(404, "Booking Web checkout command adapter is not configured."); - }, - async submitChangeRequest() { - throw createHttpError(404, "Booking Web checkout command adapter is not configured."); - }, - async getChangeRequest() { - throw createHttpError(404, "Booking Web checkout adapter is not configured."); - }, - async getPaymentInstructions() { - throw createHttpError(404, "Booking-scoped payment instructions are not configured."); - }, - async validatePromo(_slug, request) { - const code = typeof request.code === "string" ? request.code.trim() : ""; - if (!code) { - return { valid: false, code, message: "Promo code is required" }; - } - throw createHttpError(404, "Booking Web promo validation adapter is not configured."); - }, - }; -} - async function resolveTargetCheckoutProperty( pool: BookingWebQueryExecutor, slug: string, diff --git a/apps/api/src/routes/bookingWebPublicParity.test.ts b/apps/api/src/routes/bookingWebPublicParity.test.ts index 80375ed629..1a15cc7b5b 100644 --- a/apps/api/src/routes/bookingWebPublicParity.test.ts +++ b/apps/api/src/routes/bookingWebPublicParity.test.ts @@ -25,7 +25,6 @@ import type { } from "./bookingWebAffiliate.js"; import { createTargetBookingWebCheckoutAdapter, - createUnavailableBookingWebCheckoutAdapter, recordTargetCheckoutCommand, resolveTargetCheckoutAmountSnapshot, targetCardPaymentIdempotencyKey, @@ -35,6 +34,8 @@ import { type BookingWebCheckoutAdapter, } from "./bookingWebPublic.js"; +const unusedBookingWebCheckoutAdapter = {} as BookingWebCheckoutAdapter; + type LegacyHotelResponse = { id: string; name: string; @@ -203,6 +204,7 @@ describe("Booking Web public bootstrap parity", () => { const app = buildApp({ logger: false, publicHotelProfileRepository: createProfileRepository(legacyHotel, {}), + bookingWebCheckoutAdapter: unusedBookingWebCheckoutAdapter, bookingWebPublicNow: () => new Date("2026-06-06T11:00:00.000Z"), bookingWebAttributionSink: { async recordAffiliateClick(event) { @@ -239,6 +241,7 @@ describe("Booking Web public bootstrap parity", () => { const app = buildApp({ logger: false, publicHotelProfileRepository: createProfileRepository(legacyHotel, {}), + bookingWebCheckoutAdapter: unusedBookingWebCheckoutAdapter, bookingWebPublicNow: () => new Date("2026-06-06T11:00:00.000Z"), bookingWebAttributionSink: { async recordAffiliateClick() {}, @@ -279,6 +282,7 @@ describe("Booking Web public bootstrap parity", () => { const app = buildApp({ logger: false, publicHotelProfileRepository: createProfileRepository(legacyHotel, {}), + bookingWebCheckoutAdapter: unusedBookingWebCheckoutAdapter, bookingWebAttributionSink: { async recordAffiliateClick() {}, async recordTelemetryEvent(event) { @@ -302,6 +306,7 @@ describe("Booking Web public bootstrap parity", () => { const app = buildApp({ logger: false, publicHotelProfileRepository: createProfileRepository(legacyHotel, {}), + bookingWebCheckoutAdapter: unusedBookingWebCheckoutAdapter, }); const response = await app.inject({ @@ -440,124 +445,12 @@ describe("Booking Web public bootstrap parity", () => { await app.close(); }); - it("fails closed for checkout lifecycle routes without a target checkout adapter", async () => { - const app = buildApp({ - logger: false, - publicHotelProfileRepository: createProfileRepository(legacyHotel, {}), - }); - - const checkoutConfig = await app.inject({ - method: "GET", - url: "/api/booking-web/hotels/hotel-alpenrose/checkout-config", - }); - const create = await app.inject({ - method: "POST", - url: "/api/booking-web/hotels/hotel-alpenrose/bookings", - payload: { - roomTypeId: "room_deluxe", - guestEmail: "guest@example.com", - checkIn: "2026-09-12", - checkOut: "2026-09-13", - paymentMethod: "pay_at_property", - }, - }); - const quote = await app.inject({ - method: "POST", - url: "/api/booking-web/hotels/hotel-alpenrose/bookings/quote", - payload: { - roomTypeId: "room_deluxe", - guestEmail: "guest@example.com", - checkIn: "2026-09-12", - checkOut: "2026-09-15", - paymentMethod: "pay_at_property", - }, - }); - const confirm = await app.inject({ - method: "POST", - url: "/api/booking-web/hotels/hotel-alpenrose/bookings/draft_1/confirm-authorization", - }); - const status = await app.inject({ - method: "GET", - url: "/api/booking-web/hotels/hotel-alpenrose/bookings/status?reference=ALP-1001&email=guest%40example.com", - }); - const lookup = await app.inject({ - method: "POST", - url: "/api/booking-web/hotels/hotel-alpenrose/bookings/lookup", - payload: { bookingReference: "ALP-1001", guestEmail: "guest@example.com" }, - }); - const paymentInstructions = await app.inject({ - method: "GET", - url: "/api/booking-web/hotels/hotel-alpenrose/bookings/ALP-1001/payment-instructions", - }); - const promo = await app.inject({ - method: "POST", - url: "/api/booking-web/hotels/hotel-alpenrose/promo/validate", - payload: { code: "SUMMER10" }, - }); - - expect([ - checkoutConfig.statusCode, - create.statusCode, - quote.statusCode, - confirm.statusCode, - status.statusCode, - lookup.statusCode, - promo.statusCode, - ]).toEqual([404, 404, 404, 400, 404, 404, 404]); - expect(paymentInstructions.statusCode).toBe(404); - expect(create.json()).toMatchObject({ - message: "Booking Web checkout command adapter is not configured.", - }); - expect(promo.json()).toMatchObject({ - message: "Booking Web promo validation adapter is not configured.", - }); - await app.close(); - }); - - it("does not proxy checkout commands to legacy PMS unless explicitly enabled", async () => { - const app = buildApp({ - logger: false, - publicHotelProfileRepository: createProfileRepository(legacyHotel, {}), - }); - - const create = await app.inject({ - method: "POST", - url: "/api/booking-web/hotels/hotel-alpenrose/bookings", - payload: { guestEmail: "guest@example.com" }, - }); - const confirm = await app.inject({ - method: "POST", - url: "/api/booking-web/hotels/hotel-alpenrose/bookings/draft_1/confirm-authorization", - }); - const withdraw = await app.inject({ - method: "POST", - url: "/api/booking-web/hotels/hotel-alpenrose/bookings/booking_pending/withdraw", - payload: { guestEmail: "guest@example.com" }, - }); - const cancelPreview = await app.inject({ - method: "POST", - url: "/api/booking-web/hotels/hotel-alpenrose/bookings/booking_1/cancel-preview", - payload: { guestEmail: "guest@example.com" }, - }); - const changePreview = await app.inject({ - method: "POST", - url: "/api/booking-web/hotels/hotel-alpenrose/bookings/booking_1/change-request/preview", - payload: changeRequestPayload(), - }); - - expect(create.statusCode).toBe(404); - expect(confirm.statusCode).toBe(400); - expect(withdraw.statusCode).toBe(404); - expect(cancelPreview.statusCode).toBe(404); - expect(changePreview.statusCode).toBe(404); - await app.close(); - }); - it("serves target-owned affiliate routes without PMS public API config", async () => { const affiliateRepository = new InMemoryAffiliateRepository(); const app = buildApp({ logger: false, publicHotelProfileRepository: createProfileRepository(legacyHotel, {}), + bookingWebCheckoutAdapter: unusedBookingWebCheckoutAdapter, bookingWebAffiliateRepository: affiliateRepository, }); @@ -959,7 +852,7 @@ describe("Booking Web public bootstrap parity", () => { logger: false, publicHotelProfileRepository: createProfileRepository(legacyHotel, {}), bookingWebCheckoutAdapter: { - ...createUnavailableBookingWebCheckoutAdapter(), + ...unusedBookingWebCheckoutAdapter, async consumeLookupAttempt() { throw Object.assign(new Error("Too many booking lookup attempts."), { statusCode: 429 }); }, @@ -987,7 +880,7 @@ describe("Booking Web public bootstrap parity", () => { trustProxy: ["10.0.0.1"], publicHotelProfileRepository: createProfileRepository(legacyHotel, {}), bookingWebCheckoutAdapter: { - ...createUnavailableBookingWebCheckoutAdapter(), + ...unusedBookingWebCheckoutAdapter, async consumeLookupAttempt(hash) { hashes.push(hash); }, @@ -1021,7 +914,7 @@ describe("Booking Web public bootstrap parity", () => { logger: false, publicHotelProfileRepository: createProfileRepository(legacyHotel, {}), bookingWebCheckoutAdapter: { - ...createUnavailableBookingWebCheckoutAdapter(), + ...unusedBookingWebCheckoutAdapter, async confirmAuthorization() { called = true; return {}; @@ -1042,7 +935,7 @@ describe("Booking Web public bootstrap parity", () => { it("keeps one checkout attempt replayable while distinct attempt tokens create distinct quotes", async () => { const keys: string[] = []; const checkoutAdapter: BookingWebCheckoutAdapter = { - ...createUnavailableBookingWebCheckoutAdapter(), + ...unusedBookingWebCheckoutAdapter, async quoteBooking(_slug, _request, context) { if (!context) throw new Error("Missing checkout command context"); keys.push(context.idempotencyKey); @@ -3331,6 +3224,7 @@ function buildParityApp(config: { publicHotelProfileRepository: profileRepository, publicHotelQuoteRepository: quoteRepository, bookingWebCalendarRepository: calendarRepository, + bookingWebCheckoutAdapter: unusedBookingWebCheckoutAdapter, bookingWebPublicNow: () => new Date("2026-06-06T11:00:00.000Z"), }); } diff --git a/apps/api/src/server.ts b/apps/api/src/server.ts index 1926eb5cbd..d951a330c8 100644 --- a/apps/api/src/server.ts +++ b/apps/api/src/server.ts @@ -301,19 +301,16 @@ const stripeBookingPaymentProvider = config.stripeSubscriptions.secretKey ? createStripeBookingPaymentProvider({ secretKey: config.stripeSubscriptions.secretKey }) : undefined; -const bookingWebCheckoutAdapter = - config.bookingCheckoutCommandSource === "target" - ? createTargetBookingWebCheckoutAdapter({ - connectionString: targetDatabaseUrl, - inventoryReservationPort: createTargetPmsInventoryReservationPort(), - billingConfigReadPortFactory: (executor) => - createTargetFinanceBillingConfigReadPort({ - connectionString: targetDatabaseUrl, - pool: executor, - }), - stripePaymentProvider: stripeBookingPaymentProvider, - }) - : undefined; +const bookingWebCheckoutAdapter = createTargetBookingWebCheckoutAdapter({ + connectionString: targetDatabaseUrl, + inventoryReservationPort: createTargetPmsInventoryReservationPort(), + billingConfigReadPortFactory: (executor) => + createTargetFinanceBillingConfigReadPort({ + connectionString: targetDatabaseUrl, + pool: executor, + }), + stripePaymentProvider: stripeBookingPaymentProvider, +}); const pmsOperationsRepository = config.pmsOperationsSource === "target" @@ -1476,17 +1473,14 @@ if (platformMediaRuntime) { }); } -const bookingLifecycleStore = - config.bookingCheckoutCommandSource === "target" - ? createPgBookingLifecycleStore({ - connectionString: targetDatabaseUrl, - inventoryReservationPort: createTargetPmsInventoryReservationPort(), - stripePaymentProvider: stripeBookingPaymentProvider, - }) - : undefined; +const bookingLifecycleStore = createPgBookingLifecycleStore({ + connectionString: targetDatabaseUrl, + inventoryReservationPort: createTargetPmsInventoryReservationPort(), + stripePaymentProvider: stripeBookingPaymentProvider, +}); let activeBookingLifecycleRun: Promise | undefined; const runBookingLifecycle = () => { - if (!bookingLifecycleStore || activeBookingLifecycleRun) return; + if (activeBookingLifecycleRun) return; activeBookingLifecycleRun = runBookingLifecycleSchedulerJobs(bookingLifecycleStore) .then((result) => { if (result.failed > 0) { @@ -1501,11 +1495,9 @@ const runBookingLifecycle = () => { activeBookingLifecycleRun = undefined; }); }; -const bookingLifecycleTimer = bookingLifecycleStore - ? setInterval(runBookingLifecycle, 60_000) - : undefined; -bookingLifecycleTimer?.unref(); -if (bookingLifecycleStore) runBookingLifecycle(); +const bookingLifecycleTimer = setInterval(runBookingLifecycle, 60_000); +bookingLifecycleTimer.unref(); +runBookingLifecycle(); const bookingEmailDelivery = config.bookingEmailDelivery ? createResendBookingEmailDelivery(config.bookingEmailDelivery) @@ -1530,11 +1522,11 @@ const bookingEmailDeliveryTimer = bookingEmailDelivery bookingEmailDeliveryTimer?.unref(); if (bookingEmailDelivery) runBookingEmailDelivery(); app.addHook("onClose", async () => { - if (bookingLifecycleTimer) clearInterval(bookingLifecycleTimer); + clearInterval(bookingLifecycleTimer); if (bookingEmailDeliveryTimer) clearInterval(bookingEmailDeliveryTimer); await activeBookingLifecycleRun; await activeBookingEmailDelivery; - await bookingLifecycleStore?.close(); + await bookingLifecycleStore.close(); }); const propertySetupDraftRetentionWorker = startPropertySetupDraftRetentionWorker({ diff --git a/engineering/apps-api-legacy-runtime-dependency-audit.md b/engineering/apps-api-legacy-runtime-dependency-audit.md index ae6e52b216..fd8d11422d 100644 --- a/engineering/apps-api-legacy-runtime-dependency-audit.md +++ b/engineering/apps-api-legacy-runtime-dependency-audit.md @@ -1,6 +1,6 @@ # apps/api legacy data-source adapter audit -_Updated 2026-08-19 after removing the public-bookability compatibility selector._ +_Updated 2026-08-19 after removing all safe legacy runtime compatibility selectors._ ## Scope @@ -25,23 +25,25 @@ composition. resolution always delegates to that selected target profile repository. - Public quotes and calendars always use their target repositories; the unavailable-quote compatibility adapter is deleted. +- Booking Web checkout commands and lifecycle jobs always use their target + adapters; the unavailable checkout adapter is deleted. - `apps/api` has no live legacy Python API client. The old Booking/PMS service URL names are rejected during configuration loading in every runtime. - `API_RUNTIME=next` always resolves Booking Web hosts, quotes, and calendars - through target repositories and requires target Finance, PMS operations, and - checkout sources. Booking settings, reservations, and public bookability no + through target repositories and requires target Finance and PMS operations. + Booking settings, reservations, public bookability, and checkout commands no longer have source selectors. ## Runtime inventory -| Configuration key | Code entry point | Old database/service used | Route or user flow | Current next-runtime behavior | Safe to remove? | -| ------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Removed: `BOOKING_RESERVATIONS_READ_DATABASE_URL` plus `BOOKING_RESERVATIONS_SOURCE=legacy` | Removed `createCompatibilityPmsBookingReservationsReadRepository` in `src/routes/bookingReservations.ts`; `src/server.ts` now directly composes `createTargetBookingReservationsReadRepository` | Legacy `pms-api` database tables `bookings`, `room_types`, `rooms`, and `booking_rooms` | Booking Admin `GET /api/booking/hotels/:hotelId/reservations`, including status/search pagination and assigned-room projection | Always reads Booking, PMS operations, and Finance projections through `TARGET_DATABASE_URL`; route contract and authorization policy are unchanged. Target dashboard metrics remain gated to the next runtime | **Removed in this change.** The next runtime already required the target source. | -| Removed: `PUBLIC_HOTEL_PROFILE_SOURCE=legacy` and its `BOOKING_DATABASE_URL` reader | `createPublicRuntimeRepositories` now selects only `createTargetPublicHotelProfileRepository` or `createActiveBookingPublicationProfileRepository` | None; both implementations use `TARGET_DATABASE_URL` | Public AI hotel profile and Booking Web hotel profile | Always target-backed; `PUBLIC_HOTEL_PROFILE_SOURCE` accepts only `target` or `active_publication`. | **Removed.** `createPgPublicHotelProfileRepository`, its `booking_hotels` queries, and the obsolete database URL are deleted. | -| Removed: legacy Booking settings adapter, source selector, and `BOOKING_DATABASE_URL` | `src/server.ts` directly composes `createPgTargetBookingSettingsRepository` in `src/routes/bookingSettings.ts` | None | Booking Admin property, add-on, guest-form, benefits, localization, room-filter, design, and last-minute settings GET/PUT flows | Always reads and writes `booking.booking_settings`; route contracts and authorization policies are unchanged. | **Removed.** No legacy Booking product database configuration remains in `apps/api`. | -| Removed: `PUBLIC_BOOKABILITY_SOURCE=legacy` and unavailable-quote adapter | `createPublicRuntimeRepositories` directly composes target quote and calendar repositories | None | Public AI quote, Booking Web offers, and calendar | Always target-backed; target repositories retain contract-valid unavailable reasons for invalid or unready data. | **Removed.** The target response shapes and public-safety checks are unchanged. | -| Removed: `BOOKING_DOMAIN_RESOLUTION_SOURCE=legacy` | `findProfileForHost` in `src/routes/bookingWebPublic.ts` directly uses the selected profile repository | None | Booking Web `GET /api/booking-web/hosts/:host` | Known booking hosts resolve by slug; other hosts resolve through `findProfileByCustomDomain` on the target profile repository. | **Removed with the profile cleanup.** The duplicate legacy/target branches had the same repository behavior. | -| `BOOKING_CHECKOUT_COMMAND_SOURCE=legacy_proxy` | `src/server.ts` leaves `bookingWebCheckoutAdapter` undefined, so `registerBookingWebPublicRoutes` installs `createUnavailableBookingWebCheckoutAdapter` | None; there is no remaining legacy proxy or HTTP client | Booking Web checkout, booking lookup/status, cancellation/change, payment instructions, and promo routes return the unavailable contract | Unreachable: next requires `BOOKING_CHECKOUT_COMMAND_SOURCE=target` | **Yes after confirming legacy-runtime disabled behavior is not needed**, but it is a separate public write-surface cleanup, not a database-adapter removal. | +| Configuration key | Code entry point | Old database/service used | Route or user flow | Current next-runtime behavior | Safe to remove? | +| ------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------- | +| Removed: `BOOKING_RESERVATIONS_READ_DATABASE_URL` plus `BOOKING_RESERVATIONS_SOURCE=legacy` | Removed `createCompatibilityPmsBookingReservationsReadRepository` in `src/routes/bookingReservations.ts`; `src/server.ts` now directly composes `createTargetBookingReservationsReadRepository` | Legacy `pms-api` database tables `bookings`, `room_types`, `rooms`, and `booking_rooms` | Booking Admin `GET /api/booking/hotels/:hotelId/reservations`, including status/search pagination and assigned-room projection | Always reads Booking, PMS operations, and Finance projections through `TARGET_DATABASE_URL`; route contract and authorization policy are unchanged. Target dashboard metrics remain gated to the next runtime | **Removed in this change.** The next runtime already required the target source. | +| Removed: `PUBLIC_HOTEL_PROFILE_SOURCE=legacy` and its `BOOKING_DATABASE_URL` reader | `createPublicRuntimeRepositories` now selects only `createTargetPublicHotelProfileRepository` or `createActiveBookingPublicationProfileRepository` | None; both implementations use `TARGET_DATABASE_URL` | Public AI hotel profile and Booking Web hotel profile | Always target-backed; `PUBLIC_HOTEL_PROFILE_SOURCE` accepts only `target` or `active_publication`. | **Removed.** `createPgPublicHotelProfileRepository`, its `booking_hotels` queries, and the obsolete database URL are deleted. | +| Removed: legacy Booking settings adapter, source selector, and `BOOKING_DATABASE_URL` | `src/server.ts` directly composes `createPgTargetBookingSettingsRepository` in `src/routes/bookingSettings.ts` | None | Booking Admin property, add-on, guest-form, benefits, localization, room-filter, design, and last-minute settings GET/PUT flows | Always reads and writes `booking.booking_settings`; route contracts and authorization policies are unchanged. | **Removed.** No legacy Booking product database configuration remains in `apps/api`. | +| Removed: `PUBLIC_BOOKABILITY_SOURCE=legacy` and unavailable-quote adapter | `createPublicRuntimeRepositories` directly composes target quote and calendar repositories | None | Public AI quote, Booking Web offers, and calendar | Always target-backed; target repositories retain contract-valid unavailable reasons for invalid or unready data. | **Removed.** The target response shapes and public-safety checks are unchanged. | +| Removed: `BOOKING_DOMAIN_RESOLUTION_SOURCE=legacy` | `findProfileForHost` in `src/routes/bookingWebPublic.ts` directly uses the selected profile repository | None | Booking Web `GET /api/booking-web/hosts/:host` | Known booking hosts resolve by slug; other hosts resolve through `findProfileByCustomDomain` on the target profile repository. | **Removed with the profile cleanup.** The duplicate legacy/target branches had the same repository behavior. | +| Removed: `BOOKING_CHECKOUT_COMMAND_SOURCE=legacy_proxy` and unavailable adapter | `src/server.ts` directly composes `createTargetBookingWebCheckoutAdapter` and `createPgBookingLifecycleStore` | None | Booking Web checkout, booking lookup/status, cancellation/change, payment instructions, and promo routes | Always target-backed; target authorization, idempotency, payment, reservation handoff, and guest-visible response contracts are unchanged. | **Removed.** No legacy proxy or disabled checkout runtime branch remains. | ## Rejected legacy service configuration @@ -83,6 +85,6 @@ Keeping the guard is useful because it detects stale deployment configuration. ## Smallest follow-up scope -Remove `BOOKING_CHECKOUT_COMMAND_SOURCE=legacy_proxy`, the remaining no-service -compatibility selector, in its own route-focused change. The domain-resolution -selector was removed with the profile reader. +All runtime adapters and selectors marked safe to remove by this audit are now +deleted. The three stale Python service URL rejection guards remain +intentionally in place. diff --git a/engineering/booking-pms-route-migration-inventory.md b/engineering/booking-pms-route-migration-inventory.md index 78c69f69c9..8c9603530c 100644 --- a/engineering/booking-pms-route-migration-inventory.md +++ b/engineering/booking-pms-route-migration-inventory.md @@ -48,7 +48,7 @@ behavior is called out per route: | `GET /api/ai/hotels/:slug/quote` and `/api/booking-web/hotels/:slug/offers` | Public room/rate quote. | Always reads the target projection; target validation/readiness failures retain the contract-valid unavailable response shape. | | `GET /api/booking-web/hotels/:slug/calendar` | Public unavailable dates. | Always reads the target calendar projection. | | `GET /api/booking-web/hosts/:host` | Booking host/custom-domain resolution. | Contract uses the selected target profile repository for known subdomains and verified custom domains; the legacy domain selector is removed. | -| Booking Web checkout, guest command, promo, affiliate routes under `/api/booking-web` | PMS public checkout routes, Booking promo validation, PMS affiliate public routes. | Target adapters own migrated behavior; compatibility mode returns explicit unavailable responses without a legacy Python client. | +| Booking Web checkout, guest command, promo, affiliate routes under `/api/booking-web` | PMS public checkout routes, Booking promo validation, PMS affiliate public routes. | Checkout commands always use target adapters. Affiliate routes retain their separate target-repository availability behavior. | | `POST /api/booking-web/events` and `/hotels/:slug/attribution/clicks` | Booking public telemetry and affiliate click tracking. | Target event sink writes platform events/audit rows; legacy telemetry forwarding to `BOOKING_PUBLIC_API_URL /api/events` is retired. | ## booking-api route inventory @@ -108,8 +108,8 @@ behavior is called out per route: | Routes | Consumers | apps/api status | Disposition | | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `GET /api/hotels/{slug}/rooms`, `GET /api/hotels/{slug}/unavailable-dates` | BW, API quote/calendar adapters | Proxied by `apps/api` quote/offers/calendar | Distribution/bookability read vertical. Replace with target offer/calendar read models; Channex/availability freshness is cutover-window sensitive. | -| `POST /api/hotels/{slug}/bookings`, `POST /api/hotels/{slug}/bookings/{handle}/confirm-authorization`, `POST /api/hotels/{slug}/bookings/{booking_id}/withdraw`, `POST /api/hotels/{slug}/bookings/{booking_id}/cancel-preview`, `POST /api/hotels/{slug}/bookings/{booking_id}/cancel`, `POST /api/hotels/{slug}/bookings/lookup`, `GET /api/hotels/{slug}/bookings/status`, `POST /api/hotels/{slug}/bookings/{booking_id}/change-request/preview`, `POST /api/hotels/{slug}/bookings/{booking_id}/change-request`, `GET /api/hotels/{slug}/bookings/{booking_id}/change-request`, `GET /api/hotels/{slug}/payment-settings` | BW, API checkout adapter | Compatibility adapter exists; write commands proxy only when explicitly enabled | Booking/checkout vertical plus PMS reservation handoff. This is staging-rehearsal gating because guest bookings, cancellation, change requests, and payment state cannot depend on the legacy PMS public API at cutover. | +| `GET /api/hotels/{slug}/rooms`, `GET /api/hotels/{slug}/unavailable-dates` | BW, API quote/calendar adapters | Target Distribution quote/offer/calendar projections active; no legacy PMS route is called | Keep the target projections and their Channex/availability freshness contracts. | +| `POST /api/hotels/{slug}/bookings`, `POST /api/hotels/{slug}/bookings/{handle}/confirm-authorization`, `POST /api/hotels/{slug}/bookings/{booking_id}/withdraw`, `POST /api/hotels/{slug}/bookings/{booking_id}/cancel-preview`, `POST /api/hotels/{slug}/bookings/{booking_id}/cancel`, `POST /api/hotels/{slug}/bookings/lookup`, `GET /api/hotels/{slug}/bookings/status`, `POST /api/hotels/{slug}/bookings/{booking_id}/change-request/preview`, `POST /api/hotels/{slug}/bookings/{booking_id}/change-request`, `GET /api/hotels/{slug}/bookings/{booking_id}/change-request`, `GET /api/hotels/{slug}/payment-settings` | BW, API checkout adapter | Target Booking command adapter, lifecycle store, and PMS reservation handoff active | Keep target idempotency, guest-visible status mapping, payment recovery, audit, and reservation handoff contracts. The legacy public API is not a runtime dependency. | | `GET /api/hotels/{slug}/affiliates/check-email`, `POST /api/hotels/{slug}/affiliates`, `POST /api/hotels/{slug}/affiliates/{referral_code}/click`, `POST /api/hotels/{slug}/affiliates/{affiliate_id}/stripe/connect`, `GET /api/hotels/{slug}/affiliates/{affiliate_id}/stripe/onboarding-link` | BW, API affiliate adapter | `check-email`, `register`, and connect are proxied; click is replaced by API event sink; onboarding-link not exposed by API adapter | Marketplace/affiliate plus finance target ownership behind `AFFILIATE_PUBLIC_SOURCE=target`. Booking Web remains a compatibility facade while `ReferModal` is live; retire only after that public UI/product surface is removed. | #### VAY-768 affiliate route ownership decision @@ -260,15 +260,13 @@ controls is not enough for staging rehearsal. models. Custom-domain admin writes also use the target domain-verification route family. -3. **B3: Public bookability quote/calendar reads** - Replace PMS rooms and unavailable-date proxying with target distribution - read models. This is read-only but cutover-window sensitive because - availability freshness and Channex mapping state affect guest search. +3. **Completed B3: Public bookability quote/calendar reads** + Target Distribution read models serve public quotes and calendars without a + compatibility selector. -4. **B4: Booking checkout and guest lifecycle** - Port create/confirm/status/lookup/cancel/withdraw/change-request/promo flows - to Booking-owned target commands plus PMS reservation handoff. This gates - staging rehearsal because guest booking writes cannot depend on legacy PMS. +4. **Completed B4: Booking checkout and guest lifecycle** + Booking-owned target commands plus the PMS reservation handoff serve + create/confirm/status/lookup/cancel/withdraw/change-request/promo flows. 5. **P1: PMS operations inventory and reservation reads** Port rooms, room types, calendar, room blocks, reservation list/detail, and @@ -345,9 +343,9 @@ The staging rehearsal milestone is gated by these verticals: - The target quote/calendar repositories remain; the legacy source value and unavailable-quote adapter are removed. -4. **VAY follow-up: Remove the checkout-command compatibility selector** - - Scope: keep the existing target Booking command adapter and remove - `BOOKING_CHECKOUT_COMMAND_SOURCE=legacy_proxy` plus its unavailable branch. +4. **Completed: Remove the checkout-command compatibility selector** + - The target Booking command adapter remains; the legacy source value and + unavailable checkout adapter are removed. 5. **VAY follow-up: Define PMS operations route contracts for rooms, room types, calendar, and operational reservations** diff --git a/engineering/booking-web-public-api-routing.md b/engineering/booking-web-public-api-routing.md index 5863431e49..a73b2136b8 100644 --- a/engineering/booking-web-public-api-routing.md +++ b/engineering/booking-web-public-api-routing.md @@ -222,12 +222,9 @@ Compatibility rules: 3. Compatibility adapters may preserve current frontend response field names while the frontend is migrated, but the owning contract must name the target domain owner and data-source owner separately. -4. Booking lifecycle writes need a separate compatibility path from read - adapters. A target Booking/checkout route may temporarily delegate - create/confirm/withdraw/cancel/change commands to legacy FastAPI PMS routes - only as a server-side command adapter, with Booking-owned idempotency, - guest-visible status mapping, audit visibility, parity tests, and a removal - dependency on the PMS reservation sink handoff. The frontend still calls +4. Booking lifecycle writes use the target Booking command adapter and PMS + reservation handoff, with Booking-owned idempotency, guest-visible status + mapping, audit visibility, and parity tests. The frontend calls Booking/checkout, not PMS. 5. Manual-payment instructions must be covered by parity before the public payment-settings route is retired. @@ -236,33 +233,35 @@ Compatibility rules: 7. TypeScript quote and offer reads use the target Distribution projections. `PMS_PUBLIC_API_URL` is rejected as stale Python integration configuration and is not a target Booking Web dependency. -8. Booking lifecycle compatibility must preserve current guest-visible behavior +8. Target Booking lifecycle routes preserve current guest-visible behavior for card soft holds, manual payments, request-to-book states, cancellation previews, and change request previews until parity fixtures cover the target routes. -## Implementation Slices +## Recorded Migration Sequence -Recommended follow-up order: +The migration followed this order. Steps 1–5 are complete; step 6 remains for +the separately tracked affiliate compatibility surface: -1. Add Booking Web public route adapters in the TypeScript API with no direct - frontend changes. Adapters may call existing FastAPI compatibility routes - server-side while emitting target response contracts. -2. Add parity tests for current hotel page bootstrap, rooms/offers, +1. Added Booking Web public route adapters in the TypeScript API with no direct + frontend changes. The active adapters are target-backed; stale Python API + URL configuration is rejected. +2. Added parity tests for current hotel page bootstrap, rooms/offers, host/custom-domain resolution, canonical redirects, unavailable dates, payment config, manual-payment instructions, booking create/confirm/status/lookup, cancel, change request, and affiliate click behavior. -3. Switch `apps/booking-web/services/api` from `pms` to the Booking/distribution - API client one surface at a time, starting with read-only page/offer/calendar - calls. -4. Move booking create/confirm/status/lookup/cancel/change calls behind - Booking/checkout route adapters and wire PMS handoff through the PMS +3. Switched `apps/booking-web/services/api` from `pms` to the + Booking/distribution API for page, offer, and calendar reads. The remaining + affiliate facade is tracked separately. +4. Moved booking create/confirm/status/lookup/cancel/change calls behind + Booking/checkout route adapters and wired PMS handoff through the PMS reservation sink. -5. Replace affiliate click writes with marketplace attribution events and +5. Replaced affiliate click writes with marketplace attribution events and durable telemetry/audit intake. -6. Remove `NEXT_PUBLIC_PMS_URL` from Booking Web and retire the PMS public - compatibility routes after production parity and smoke coverage are accepted. +6. Remove `NEXT_PUBLIC_PMS_URL` from Booking Web and retire the remaining PMS + public compatibility routes after production parity and smoke coverage are + accepted. ## Acceptance for Future PRs diff --git a/engineering/next-stack-legacy-dependency-inventory.md b/engineering/next-stack-legacy-dependency-inventory.md index d0a3ffb50b..17d42b3fa6 100644 --- a/engineering/next-stack-legacy-dependency-inventory.md +++ b/engineering/next-stack-legacy-dependency-inventory.md @@ -13,7 +13,8 @@ This complements: > profile and Booking settings source selectors are removed from `apps/api`. > The legacy Booking settings repository and `BOOKING_DATABASE_URL` are also > removed. Public bookability now directly composes the target quote/calendar -> repositories with no compatibility selector. +> repositories with no compatibility selector. Booking Web checkout commands +> and lifecycle jobs likewise compose only target adapters. > Historical entries below describe the earlier VAY-880 state. ## Scope diff --git a/scripts/dev-workos-local.sh b/scripts/dev-workos-local.sh index dbcaf6297b..490c832ec0 100755 --- a/scripts/dev-workos-local.sh +++ b/scripts/dev-workos-local.sh @@ -187,7 +187,6 @@ export TARGET_DATABASE_URL="${TARGET_DATABASE_URL:-${AUTH_DATABASE_URL:-}}" export API_RUNTIME="${API_RUNTIME:-next}" export PUBLIC_HOTEL_PROFILE_SOURCE="${PUBLIC_HOTEL_PROFILE_SOURCE:-target}" export FINANCE_SOURCE="${FINANCE_SOURCE:-target}" -export BOOKING_CHECKOUT_COMMAND_SOURCE="${BOOKING_CHECKOUT_COMMAND_SOURCE:-target}" export BOOKING_HOST_BASE="${BOOKING_HOST_BASE:-${BOOKING_PORTLESS_HOST}}" export PMS_OPERATIONS_SOURCE="${PMS_OPERATIONS_SOURCE:-target}" export PMS_OPERATIONS_ALLOWED_ORIGINS="${PMS_OPERATIONS_ALLOWED_ORIGINS:-${PMS_ORIGIN},${BOOKING_ADMIN_ORIGIN},${MARKETPLACE_ORIGIN}}" From a97e7ef66d487afa558b8ee92f263fa3079deac5 Mon Sep 17 00:00:00 2001 From: Flamur Maliqi Date: Wed, 19 Aug 2026 20:57:43 +0200 Subject: [PATCH 2/2] docs: mark checkout proxy flag retired --- engineering/next-stack-legacy-dependency-inventory.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/engineering/next-stack-legacy-dependency-inventory.md b/engineering/next-stack-legacy-dependency-inventory.md index 17d42b3fa6..86c1bcd4a9 100644 --- a/engineering/next-stack-legacy-dependency-inventory.md +++ b/engineering/next-stack-legacy-dependency-inventory.md @@ -55,7 +55,7 @@ The next-stack debt falls into four groups: `PUBLIC_HOTEL_PROFILE_SOURCE=legacy`, `BOOKING_*_SOURCE=legacy`, `PMS_OPERATIONS_SOURCE=disabled`, `FINANCE_SOURCE=legacy`, `BOOKING_PUBLIC_API_URL`, `PMS_PUBLIC_API_URL`, `PMS_API_URL`, and the - legacy checkout proxy flag (lines 306-468). The concrete legacy product + now-retired legacy checkout proxy flag (lines 306-468). The concrete legacy product runtime envs are `BOOKING_DATABASE_URL`, `BOOKING_PUBLIC_API_URL`, `PMS_API_URL`, and `PMS_PUBLIC_API_URL` (lines 424-457). - **Current consumer:** all next frontends using target API routes.