Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 2 additions & 4 deletions apps/api/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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 <bookings@example.com>

# Platform media serving (VAY-821)
Expand Down
26 changes: 26 additions & 0 deletions apps/api/src/app.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ import {
import {
createTargetBookingWebCalendarRepository,
type BookingWebCalendarReadPool,
type BookingWebCheckoutAdapter,
type BookingHotelChangeRequestRepository,
} from "./routes/bookingWebPublic.js";
import type {
Expand Down Expand Up @@ -2389,6 +2390,8 @@ const publicHotelProfileRepository: PublicHotelProfileRepository = {
},
};

const unusedBookingWebCheckoutAdapter = {} as BookingWebCheckoutAdapter;

function targetPublicHotelProfileRow(): QueryResultRow {
return {
propertyId: "f6893000-0000-0000-0000-000000000001",
Expand Down Expand Up @@ -2944,6 +2947,7 @@ describe("vayada-api", () => {
app = buildApp({
logger: false,
publicHotelProfileRepository,
bookingWebCheckoutAdapter: unusedBookingWebCheckoutAdapter,
});

const response = await app.inject({
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -3054,6 +3067,7 @@ describe("vayada-api", () => {
logger: false,
publicHotelProfileRepository,
publicHotelQuoteRepository,
bookingWebCheckoutAdapter: unusedBookingWebCheckoutAdapter,
});

const response = await app.inject({
Expand Down Expand Up @@ -3091,6 +3105,7 @@ describe("vayada-api", () => {
logger: false,
publicHotelProfileRepository,
publicHotelQuoteRepository,
bookingWebCheckoutAdapter: unusedBookingWebCheckoutAdapter,
});

const response = await app.inject({
Expand Down Expand Up @@ -3131,6 +3146,7 @@ describe("vayada-api", () => {
},
},
publicHotelQuoteRepository,
bookingWebCheckoutAdapter: unusedBookingWebCheckoutAdapter,
});

const response = await injectJson(app, {
Expand Down Expand Up @@ -3170,6 +3186,7 @@ describe("vayada-api", () => {
},
},
publicHotelQuoteRepository,
bookingWebCheckoutAdapter: unusedBookingWebCheckoutAdapter,
});

const response = await injectJson(app, {
Expand Down Expand Up @@ -3208,6 +3225,7 @@ describe("vayada-api", () => {
app = buildApp({
logger: false,
publicHotelProfileRepository: targetRepository,
bookingWebCheckoutAdapter: unusedBookingWebCheckoutAdapter,
});

const aiProfile = await injectJson(app, {
Expand Down Expand Up @@ -3251,6 +3269,7 @@ describe("vayada-api", () => {
},
},
publicHotelQuoteRepository,
bookingWebCheckoutAdapter: unusedBookingWebCheckoutAdapter,
});

const response = await injectJson(app, {
Expand All @@ -3266,6 +3285,7 @@ describe("vayada-api", () => {
logger: false,
publicHotelProfileRepository,
publicHotelQuoteRepository,
bookingWebCheckoutAdapter: unusedBookingWebCheckoutAdapter,
});

const response = await app.inject({
Expand Down Expand Up @@ -3297,6 +3317,7 @@ describe("vayada-api", () => {
logger: false,
publicHotelProfileRepository,
publicHotelQuoteRepository,
bookingWebCheckoutAdapter: unusedBookingWebCheckoutAdapter,
});

const preflight = await app.inject({
Expand Down Expand Up @@ -3340,6 +3361,7 @@ describe("vayada-api", () => {
logger: false,
publicHotelProfileRepository,
publicHotelQuoteRepository,
bookingWebCheckoutAdapter: unusedBookingWebCheckoutAdapter,
});

const preflight = await app.inject({
Expand Down Expand Up @@ -3373,6 +3395,7 @@ describe("vayada-api", () => {
logger: false,
publicHotelProfileRepository,
publicHotelQuoteRepository,
bookingWebCheckoutAdapter: unusedBookingWebCheckoutAdapter,
});

const response = await app.inject({
Expand Down Expand Up @@ -3414,6 +3437,7 @@ describe("vayada-api", () => {
logger: false,
publicHotelProfileRepository,
publicHotelQuoteRepository,
bookingWebCheckoutAdapter: unusedBookingWebCheckoutAdapter,
bookingWebPublicNow: () => new Date("2026-06-06T11:00:00.000Z"),
});

Expand Down Expand Up @@ -3474,6 +3498,7 @@ describe("vayada-api", () => {
logger: false,
publicHotelProfileRepository,
publicHotelQuoteRepository,
bookingWebCheckoutAdapter: unusedBookingWebCheckoutAdapter,
bookingWebCalendarRepository: {
async findCalendarByHotel(hotel, query) {
return {
Expand Down Expand Up @@ -3595,6 +3620,7 @@ describe("vayada-api", () => {
return pollutedProfile;
},
},
bookingWebCheckoutAdapter: unusedBookingWebCheckoutAdapter,
});

const response = await app.inject({
Expand Down
6 changes: 5 additions & 1 deletion apps/api/src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
46 changes: 4 additions & 42 deletions apps/api/src/config.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -329,15 +329,13 @@ describe("api config", () => {
PUBLIC_HOTEL_PROFILE_SOURCE: "target",
PMS_OPERATIONS_SOURCE: "target",
FINANCE_SOURCE: "target",
BOOKING_CHECKOUT_COMMAND_SOURCE: "target",
});

expect(config).toMatchObject({
apiRuntime: "next",
publicHotelProfileSource: "target",
pmsOperationsSource: "target",
financeSource: "target",
bookingCheckoutCommandSource: "target",
});
});

Expand All @@ -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");
Expand All @@ -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",
);
});

Expand Down Expand Up @@ -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",
Expand All @@ -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);
}
Expand Down Expand Up @@ -658,34 +653,18 @@ 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");

expect(
loadConfig({
NODE_ENV: "production",
TARGET_DATABASE_URL: "postgresql://target-db",
BOOKING_CHECKOUT_COMMAND_SOURCE: "target",
RESEND_API_KEY: "re_test",
BOOKING_EMAIL_FROM: "Vayada <bookings@example.test>",
FINANCE_SOURCE: "target",
Expand All @@ -700,19 +679,18 @@ 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 <bookings@example.test>",
STRIPE_SECRET_KEY: "sk_test",
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 },
Expand All @@ -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({
Expand Down
Loading
Loading