@voyantjs/products 0.20.0 → 0.21.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/booking-engine/handler.d.ts +203 -0
- package/dist/booking-engine/handler.d.ts.map +1 -0
- package/dist/booking-engine/handler.js +330 -0
- package/dist/booking-engine/index.d.ts +8 -0
- package/dist/booking-engine/index.d.ts.map +1 -0
- package/dist/booking-engine/index.js +7 -0
- package/dist/catalog-policy.d.ts.map +1 -1
- package/dist/catalog-policy.js +15 -1
- package/dist/content-shape.d.ts +217 -0
- package/dist/content-shape.d.ts.map +1 -0
- package/dist/content-shape.js +159 -0
- package/dist/draft-shape.d.ts +43 -0
- package/dist/draft-shape.d.ts.map +1 -0
- package/dist/draft-shape.js +46 -0
- package/dist/events.d.ts +37 -0
- package/dist/events.d.ts.map +1 -0
- package/dist/events.js +32 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/routes-content.d.ts +74 -0
- package/dist/routes-content.d.ts.map +1 -0
- package/dist/routes-content.js +117 -0
- package/dist/routes.d.ts +40 -20
- package/dist/routes.d.ts.map +1 -1
- package/dist/routes.js +83 -13
- package/dist/schema-core.d.ts +240 -1
- package/dist/schema-core.d.ts.map +1 -1
- package/dist/schema-core.js +49 -0
- package/dist/schema-itinerary.d.ts +18 -1
- package/dist/schema-itinerary.d.ts.map +1 -1
- package/dist/schema-itinerary.js +1 -0
- package/dist/schema-settings.d.ts +1 -1
- package/dist/schema-sourced-content.d.ts +262 -0
- package/dist/schema-sourced-content.d.ts.map +1 -0
- package/dist/schema-sourced-content.js +69 -0
- package/dist/schema-taxonomy.d.ts +17 -0
- package/dist/schema-taxonomy.d.ts.map +1 -1
- package/dist/schema-taxonomy.js +13 -0
- package/dist/schema.d.ts +1 -0
- package/dist/schema.d.ts.map +1 -1
- package/dist/schema.js +1 -0
- package/dist/service-catalog-plane.d.ts.map +1 -1
- package/dist/service-catalog-plane.js +1 -0
- package/dist/service-content-owned.d.ts +68 -0
- package/dist/service-content-owned.d.ts.map +1 -0
- package/dist/service-content-owned.js +224 -0
- package/dist/service-content-synthesizer.d.ts +90 -0
- package/dist/service-content-synthesizer.d.ts.map +1 -0
- package/dist/service-content-synthesizer.js +171 -0
- package/dist/service-content.d.ts +106 -0
- package/dist/service-content.d.ts.map +1 -0
- package/dist/service-content.js +365 -0
- package/dist/service.d.ts +76 -22
- package/dist/service.d.ts.map +1 -1
- package/dist/service.js +4 -0
- package/dist/tasks/brochures.d.ts +1 -0
- package/dist/tasks/brochures.d.ts.map +1 -1
- package/dist/tasks/brochures.js +3 -0
- package/dist/validation-catalog.d.ts +4 -4
- package/dist/validation-config.d.ts +3 -3
- package/dist/validation-content.d.ts +34 -4
- package/dist/validation-content.d.ts.map +1 -1
- package/dist/validation-content.js +13 -0
- package/dist/validation-core.d.ts +53 -3
- package/dist/validation-core.d.ts.map +1 -1
- package/dist/validation-core.js +16 -0
- package/dist/validation-public.d.ts +9 -9
- package/dist/validation-shared.d.ts +4 -4
- package/package.json +12 -7
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Owned-arm booking handler for the `products` vertical.
|
|
3
|
+
*
|
|
4
|
+
* Per `docs/architecture/booking-journey-architecture.md` §6 +
|
|
5
|
+
* §10 Phase A. Composes:
|
|
6
|
+
*
|
|
7
|
+
* - The products vertical's existing pricing primitives
|
|
8
|
+
* (`products.sellAmountCents` / `sellCurrency`) for pricing
|
|
9
|
+
* basis. Per-pax / per-band pricing layered in Phase C+ via
|
|
10
|
+
* `product_pax_pricing_tiers`.
|
|
11
|
+
* - `getProductContent` + `buildProductDraftShape` for the journey
|
|
12
|
+
* wizard's step descriptor.
|
|
13
|
+
* - An injected `quickCreateBooking` function for the commit path
|
|
14
|
+
* — keeps `@voyantjs/products` from depending on
|
|
15
|
+
* `@voyantjs/finance` (no workspace cycle).
|
|
16
|
+
*
|
|
17
|
+
* Phase A scope (deliberately narrow):
|
|
18
|
+
* - Price = product.sellAmountCents × pax_count, no taxes / addons /
|
|
19
|
+
* accommodation / vouchers.
|
|
20
|
+
* - Commit goes through the bridge into `bookingsQuickCreate`'s input
|
|
21
|
+
* shape — products-only, no extras / hospitality / cruises / encrypted
|
|
22
|
+
* travel details / snapshot graph.
|
|
23
|
+
*
|
|
24
|
+
* Phase C+ extensions land on this same handler without re-architecting
|
|
25
|
+
* the dispatch.
|
|
26
|
+
*/
|
|
27
|
+
import { type AddonOffer, type BookingDraftShape, type OwnedBookingHandler, type OwnedHandlerContext, type TravelerFieldRequirement } from "@voyantjs/catalog/booking-engine";
|
|
28
|
+
/**
|
|
29
|
+
* Subset of `bookingsQuickCreate`'s input the bridge builds.
|
|
30
|
+
* Mirrors the schema in `service-bookings-quick-create.ts` — kept
|
|
31
|
+
* structural here so we don't pull a dependency into products.
|
|
32
|
+
*/
|
|
33
|
+
export interface QuickCreateBridgeInput {
|
|
34
|
+
productId: string;
|
|
35
|
+
optionId?: string | null;
|
|
36
|
+
slotId?: string | null;
|
|
37
|
+
bookingNumber: string;
|
|
38
|
+
personId?: string | null;
|
|
39
|
+
organizationId?: string | null;
|
|
40
|
+
internalNotes?: string | null;
|
|
41
|
+
travelers?: Array<{
|
|
42
|
+
firstName: string;
|
|
43
|
+
lastName: string;
|
|
44
|
+
email?: string | null;
|
|
45
|
+
phone?: string | null;
|
|
46
|
+
personId?: string | null;
|
|
47
|
+
participantType: "traveler" | "occupant" | "other";
|
|
48
|
+
travelerCategory?: "adult" | "child" | "infant" | "senior" | "other" | null;
|
|
49
|
+
isPrimary?: boolean | null;
|
|
50
|
+
}>;
|
|
51
|
+
paymentSchedules?: Array<{
|
|
52
|
+
scheduleType: "deposit" | "installment" | "balance" | "hold" | "other";
|
|
53
|
+
status: "pending" | "due" | "paid" | "waived" | "cancelled" | "expired";
|
|
54
|
+
dueDate: string;
|
|
55
|
+
currency: string;
|
|
56
|
+
amountCents: number;
|
|
57
|
+
notes?: string | null;
|
|
58
|
+
}>;
|
|
59
|
+
taxLines?: Array<{
|
|
60
|
+
code?: string | null;
|
|
61
|
+
name: string;
|
|
62
|
+
jurisdiction?: string | null;
|
|
63
|
+
scope?: "included" | "excluded" | "withheld";
|
|
64
|
+
currency: string;
|
|
65
|
+
amountCents: number;
|
|
66
|
+
rateBasisPoints?: number | null;
|
|
67
|
+
includedInPrice?: boolean;
|
|
68
|
+
remittanceParty?: string | null;
|
|
69
|
+
sortOrder?: number;
|
|
70
|
+
}>;
|
|
71
|
+
}
|
|
72
|
+
export interface QuickCreateBridgeResult {
|
|
73
|
+
status: "ok" | "product_not_found" | string;
|
|
74
|
+
bookingId?: string;
|
|
75
|
+
bookingNumber?: string;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Caller-supplied bridge to `bookingsQuickCreate`. Templates wire
|
|
79
|
+
* this up — `(input, opts) => quickCreateBooking(db as PostgresJsDatabase, input, opts)`.
|
|
80
|
+
*/
|
|
81
|
+
export type QuickCreateBridge = (input: QuickCreateBridgeInput, options?: {
|
|
82
|
+
userId?: string;
|
|
83
|
+
}) => Promise<QuickCreateBridgeResult>;
|
|
84
|
+
export interface BuildOwnedProductDraftShapeOptions {
|
|
85
|
+
/**
|
|
86
|
+
* Per-traveler field requirements pulled from
|
|
87
|
+
* `@voyantjs/booking-requirements` for this product. Caller-supplied
|
|
88
|
+
* so the products package doesn't depend on booking-requirements.
|
|
89
|
+
*/
|
|
90
|
+
travelerFields?: ReadonlyArray<TravelerFieldRequirement>;
|
|
91
|
+
/**
|
|
92
|
+
* Add-on catalog projected from extras. Caller-supplied so
|
|
93
|
+
* products doesn't depend on `@voyantjs/extras`. When omitted,
|
|
94
|
+
* `showsAddons` is false.
|
|
95
|
+
*/
|
|
96
|
+
addonCatalog?: ReadonlyArray<AddonOffer>;
|
|
97
|
+
}
|
|
98
|
+
export declare function buildOwnedProductDraftShape(options?: BuildOwnedProductDraftShapeOptions): BookingDraftShape;
|
|
99
|
+
/** A resolved tax-rate decision — resolved from `tax_classes` ×
|
|
100
|
+
* `tax_regimes` × buyer country at quote time. */
|
|
101
|
+
export interface ResolvedTaxRate {
|
|
102
|
+
/** Stable code (e.g. "vat-ro-19", "exempt-art311"). */
|
|
103
|
+
code: string;
|
|
104
|
+
/** Display label for the breakdown. */
|
|
105
|
+
label: string;
|
|
106
|
+
/** Rate as a fraction (0..1). 0 means exempt / zero-rated. */
|
|
107
|
+
rate: number;
|
|
108
|
+
/** Whether the configured product price already includes this tax. */
|
|
109
|
+
priceMode?: "inclusive" | "exclusive";
|
|
110
|
+
}
|
|
111
|
+
/** Caller-supplied loaders for descriptor enrichment. Each is
|
|
112
|
+
* optional — when omitted the handler returns the default shape.
|
|
113
|
+
* Templates wire these to the modules they have on hand
|
|
114
|
+
* (booking-requirements, extras, finance). */
|
|
115
|
+
export interface OwnedProductsShapeLoaders {
|
|
116
|
+
/**
|
|
117
|
+
* Resolve per-traveler field requirements from
|
|
118
|
+
* @voyantjs/booking-requirements. Called per-quote so the descriptor
|
|
119
|
+
* reflects current configuration.
|
|
120
|
+
*/
|
|
121
|
+
loadTravelerFields?: (ctx: OwnedHandlerContext, productId: string) => Promise<ReadonlyArray<TravelerFieldRequirement>>;
|
|
122
|
+
/**
|
|
123
|
+
* Resolve the addon catalog for the product (typically a projection
|
|
124
|
+
* over `extras` + `option_extra_configs`). Caller-supplied to keep
|
|
125
|
+
* the products package free of an @voyantjs/extras dependency.
|
|
126
|
+
*/
|
|
127
|
+
loadAddonCatalog?: (ctx: OwnedHandlerContext, productId: string) => Promise<ReadonlyArray<AddonOffer>>;
|
|
128
|
+
/**
|
|
129
|
+
* Resolve the tax rate for a given (product, buyer country) pair.
|
|
130
|
+
* Templates wire this to a function that reads
|
|
131
|
+
* `products.tax_class_id`, `tax_classes.default_regime_id`, and
|
|
132
|
+
* `tax_regimes.rate_percent`. Returns null when tax can't be
|
|
133
|
+
* resolved — the engine renders the breakdown without a tax line.
|
|
134
|
+
*
|
|
135
|
+
* Per booking-journey-architecture §9.
|
|
136
|
+
*/
|
|
137
|
+
loadTaxRate?: (ctx: OwnedHandlerContext, args: {
|
|
138
|
+
productId: string;
|
|
139
|
+
buyerCountry?: string;
|
|
140
|
+
buyerType?: "B2C" | "B2B";
|
|
141
|
+
}) => Promise<ResolvedTaxRate | null>;
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Caller-supplied availability-hold bridge — keeps the products
|
|
145
|
+
* package free of an `@voyantjs/availability` dependency. When
|
|
146
|
+
* wired, the handler's `placeHold/extendHold/releaseHold` route
|
|
147
|
+
* through `availability_holds` (real inventory locks). When
|
|
148
|
+
* omitted, the handler falls back to stamping no-ops.
|
|
149
|
+
*/
|
|
150
|
+
export interface AvailabilityHoldBridge {
|
|
151
|
+
place: (input: {
|
|
152
|
+
draftId: string;
|
|
153
|
+
productId: string;
|
|
154
|
+
slotId: string;
|
|
155
|
+
paxCount: number;
|
|
156
|
+
ttlMs: number;
|
|
157
|
+
holdToken?: string;
|
|
158
|
+
}) => Promise<{
|
|
159
|
+
status: "ok";
|
|
160
|
+
holdToken: string;
|
|
161
|
+
expiresAt: Date;
|
|
162
|
+
} | {
|
|
163
|
+
status: "slot_not_found";
|
|
164
|
+
} | {
|
|
165
|
+
status: "insufficient_capacity";
|
|
166
|
+
remaining: number;
|
|
167
|
+
needed: number;
|
|
168
|
+
}>;
|
|
169
|
+
extend: (input: {
|
|
170
|
+
holdToken: string;
|
|
171
|
+
ttlMs: number;
|
|
172
|
+
}) => Promise<{
|
|
173
|
+
status: "ok";
|
|
174
|
+
expiresAt: Date;
|
|
175
|
+
} | {
|
|
176
|
+
status: "not_found";
|
|
177
|
+
}>;
|
|
178
|
+
release: (holdToken: string) => Promise<void>;
|
|
179
|
+
}
|
|
180
|
+
export interface CreateProductsBookingHandlerOptions extends OwnedProductsShapeLoaders {
|
|
181
|
+
/**
|
|
182
|
+
* Caller-supplied bridge to `bookingsQuickCreate`. Wired by the
|
|
183
|
+
* template at boot, since `@voyantjs/products` does not import
|
|
184
|
+
* `@voyantjs/finance`.
|
|
185
|
+
*/
|
|
186
|
+
quickCreate: QuickCreateBridge;
|
|
187
|
+
/**
|
|
188
|
+
* Generator for booking numbers. Defaults to a timestamp-based
|
|
189
|
+
* value if not supplied. Templates that have a sequence service
|
|
190
|
+
* (operator: numbering plugin) override.
|
|
191
|
+
*/
|
|
192
|
+
generateBookingNumber?: () => string;
|
|
193
|
+
/**
|
|
194
|
+
* Optional inventory-hold bridge. When wired, `placeHold`
|
|
195
|
+
* decrements `availability_slots.remainingPax` against the
|
|
196
|
+
* draft's chosen slot; `releaseHold` restores it. When omitted,
|
|
197
|
+
* the handler returns a stamping token without touching
|
|
198
|
+
* inventory.
|
|
199
|
+
*/
|
|
200
|
+
holds?: AvailabilityHoldBridge;
|
|
201
|
+
}
|
|
202
|
+
export declare function createProductsBookingHandler(options: CreateProductsBookingHandlerOptions): OwnedBookingHandler;
|
|
203
|
+
//# sourceMappingURL=handler.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"handler.d.ts","sourceRoot":"","sources":["../../src/booking-engine/handler.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,330 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Owned-arm booking handler for the `products` vertical.
|
|
3
|
+
*
|
|
4
|
+
* Per `docs/architecture/booking-journey-architecture.md` §6 +
|
|
5
|
+
* §10 Phase A. Composes:
|
|
6
|
+
*
|
|
7
|
+
* - The products vertical's existing pricing primitives
|
|
8
|
+
* (`products.sellAmountCents` / `sellCurrency`) for pricing
|
|
9
|
+
* basis. Per-pax / per-band pricing layered in Phase C+ via
|
|
10
|
+
* `product_pax_pricing_tiers`.
|
|
11
|
+
* - `getProductContent` + `buildProductDraftShape` for the journey
|
|
12
|
+
* wizard's step descriptor.
|
|
13
|
+
* - An injected `quickCreateBooking` function for the commit path
|
|
14
|
+
* — keeps `@voyantjs/products` from depending on
|
|
15
|
+
* `@voyantjs/finance` (no workspace cycle).
|
|
16
|
+
*
|
|
17
|
+
* Phase A scope (deliberately narrow):
|
|
18
|
+
* - Price = product.sellAmountCents × pax_count, no taxes / addons /
|
|
19
|
+
* accommodation / vouchers.
|
|
20
|
+
* - Commit goes through the bridge into `bookingsQuickCreate`'s input
|
|
21
|
+
* shape — products-only, no extras / hospitality / cruises / encrypted
|
|
22
|
+
* travel details / snapshot graph.
|
|
23
|
+
*
|
|
24
|
+
* Phase C+ extensions land on this same handler without re-architecting
|
|
25
|
+
* the dispatch.
|
|
26
|
+
*/
|
|
27
|
+
import { DEFAULT_PAX_BANDS, defaultBookingFields, defaultDraftShapeFlags, defaultTravelerFields, paxBandsAllowedTotalFrom, } from "@voyantjs/catalog/booking-engine";
|
|
28
|
+
import { eq } from "drizzle-orm";
|
|
29
|
+
import { products } from "../schema-core.js";
|
|
30
|
+
export function buildOwnedProductDraftShape(options = {}) {
|
|
31
|
+
const paxBands = DEFAULT_PAX_BANDS;
|
|
32
|
+
const fields = options.travelerFields ?? defaultTravelerFields();
|
|
33
|
+
const addons = options.addonCatalog ?? [];
|
|
34
|
+
const flags = defaultDraftShapeFlags();
|
|
35
|
+
return {
|
|
36
|
+
...flags,
|
|
37
|
+
showsAddons: addons.length > 0,
|
|
38
|
+
paxBands,
|
|
39
|
+
paxBandsAllowedTotal: paxBandsAllowedTotalFrom(paxBands),
|
|
40
|
+
travelerFields: fields,
|
|
41
|
+
bookingFields: defaultBookingFields(),
|
|
42
|
+
paymentIntents: ["hold", "card"],
|
|
43
|
+
configureSubSteps: [{ kind: "occupancy", bands: paxBands }],
|
|
44
|
+
addons: addons.length > 0 ? { catalog: addons } : undefined,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
export function createProductsBookingHandler(options) {
|
|
48
|
+
const generateNumber = options.generateBookingNumber ?? defaultBookingNumber;
|
|
49
|
+
return {
|
|
50
|
+
entityModule: "products",
|
|
51
|
+
async computeQuote(ctx, request) {
|
|
52
|
+
const product = await loadProduct(ctx.db, request.entityId);
|
|
53
|
+
if (!product) {
|
|
54
|
+
return { available: false, invalidReason: "product_not_found" };
|
|
55
|
+
}
|
|
56
|
+
if (product.status !== "active" && product.status !== "draft") {
|
|
57
|
+
return {
|
|
58
|
+
available: false,
|
|
59
|
+
invalidReason: `product_status_${product.status}`,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
const draft = (request.draft ?? {});
|
|
63
|
+
// Concurrent enrichment — all three calls are pure reads and
|
|
64
|
+
// don't depend on each other.
|
|
65
|
+
const [travelerFields, addonCatalog, taxRate] = await Promise.all([
|
|
66
|
+
options.loadTravelerFields?.(ctx, request.entityId) ?? Promise.resolve(undefined),
|
|
67
|
+
options.loadAddonCatalog?.(ctx, request.entityId) ?? Promise.resolve(undefined),
|
|
68
|
+
options.loadTaxRate?.(ctx, {
|
|
69
|
+
productId: request.entityId,
|
|
70
|
+
buyerCountry: draft.billing?.address?.country,
|
|
71
|
+
buyerType: draft.billing?.buyerType,
|
|
72
|
+
}) ?? Promise.resolve(null),
|
|
73
|
+
]);
|
|
74
|
+
const paxCount = sumPax(draft.configure?.pax);
|
|
75
|
+
// Per-pax pricing fallback: when no pax is supplied yet, quote a
|
|
76
|
+
// single-occupant baseline so the wizard can render a starter
|
|
77
|
+
// total before the user picks counts.
|
|
78
|
+
const effectivePax = paxCount > 0 ? paxCount : 1;
|
|
79
|
+
const unitCents = product.sellAmountCents ?? 0;
|
|
80
|
+
const grossCents = unitCents * effectivePax;
|
|
81
|
+
// Tax computation. The base is taxable; addons/accommodation
|
|
82
|
+
// get the same rate in this MVP cut. Per-line override (the
|
|
83
|
+
// `applies_to` axis on tax_classes.lines) lands in a follow-up
|
|
84
|
+
// when the catalog actually carries mixed treatments.
|
|
85
|
+
const taxIsInclusive = taxRate?.priceMode === "inclusive";
|
|
86
|
+
const taxCents = taxRate && taxRate.rate > 0
|
|
87
|
+
? taxIsInclusive
|
|
88
|
+
? Math.round(grossCents - grossCents / (1 + taxRate.rate))
|
|
89
|
+
: Math.round(grossCents * taxRate.rate)
|
|
90
|
+
: 0;
|
|
91
|
+
const netCents = taxIsInclusive ? grossCents - taxCents : grossCents;
|
|
92
|
+
const payableCents = taxIsInclusive ? grossCents : netCents + taxCents;
|
|
93
|
+
const pricing = unitCents > 0
|
|
94
|
+
? {
|
|
95
|
+
base_amount: netCents,
|
|
96
|
+
taxes: taxCents,
|
|
97
|
+
fees: 0,
|
|
98
|
+
surcharges: 0,
|
|
99
|
+
currency: product.sellCurrency,
|
|
100
|
+
breakdown: {
|
|
101
|
+
lines: [
|
|
102
|
+
{
|
|
103
|
+
kind: "base",
|
|
104
|
+
label: product.name,
|
|
105
|
+
quantity: effectivePax,
|
|
106
|
+
unitAmount: unitCents,
|
|
107
|
+
totalAmount: grossCents,
|
|
108
|
+
taxIncluded: taxIsInclusive,
|
|
109
|
+
},
|
|
110
|
+
],
|
|
111
|
+
taxes: taxRate && taxCents > 0
|
|
112
|
+
? [
|
|
113
|
+
{
|
|
114
|
+
code: taxRate.code,
|
|
115
|
+
label: taxRate.label,
|
|
116
|
+
rate: taxRate.rate,
|
|
117
|
+
amount: taxCents,
|
|
118
|
+
base: netCents,
|
|
119
|
+
includedInPrice: taxIsInclusive,
|
|
120
|
+
scope: taxIsInclusive ? "included" : "excluded",
|
|
121
|
+
},
|
|
122
|
+
]
|
|
123
|
+
: [],
|
|
124
|
+
subtotal: netCents,
|
|
125
|
+
taxTotal: taxCents,
|
|
126
|
+
total: payableCents,
|
|
127
|
+
paxCount: effectivePax,
|
|
128
|
+
},
|
|
129
|
+
}
|
|
130
|
+
: undefined;
|
|
131
|
+
return {
|
|
132
|
+
available: unitCents > 0,
|
|
133
|
+
invalidReason: unitCents > 0 ? undefined : "no_sell_amount_configured",
|
|
134
|
+
pricing,
|
|
135
|
+
shape: buildOwnedProductDraftShape({
|
|
136
|
+
travelerFields,
|
|
137
|
+
addonCatalog,
|
|
138
|
+
}),
|
|
139
|
+
};
|
|
140
|
+
},
|
|
141
|
+
/**
|
|
142
|
+
* Place a soft hold on the row's chosen slot. When the
|
|
143
|
+
* `holds` bridge is wired, decrements
|
|
144
|
+
* `availability_slots.remainingPax` against the slot for the
|
|
145
|
+
* pax count; concurrent placeHold attempts are serialized via
|
|
146
|
+
* a row-level lock inside the bridge. When omitted, returns a
|
|
147
|
+
* stamping token without touching inventory.
|
|
148
|
+
*
|
|
149
|
+
* The slot id and pax count are pulled off
|
|
150
|
+
* `request.parameters.slotId` / `request.parameters.paxCount`
|
|
151
|
+
* — the journey wizard threads these from the draft's
|
|
152
|
+
* Configure step (`departureSlotId` + summed `pax`).
|
|
153
|
+
*/
|
|
154
|
+
async placeHold(_ctx, request) {
|
|
155
|
+
const token = request.draftId ?? defaultBookingNumber();
|
|
156
|
+
const expiresAt = new Date(Date.now() + request.ttlMs);
|
|
157
|
+
if (!options.holds) {
|
|
158
|
+
return { holdToken: token, expiresAt };
|
|
159
|
+
}
|
|
160
|
+
const params = (request.parameters ?? {});
|
|
161
|
+
const slotId = params.slotId;
|
|
162
|
+
const paxCount = params.paxCount ?? 1;
|
|
163
|
+
if (!slotId || !request.draftId) {
|
|
164
|
+
// No slot chosen yet → no inventory to lock. Return a
|
|
165
|
+
// stamping token so the journey can still call extend /
|
|
166
|
+
// release.
|
|
167
|
+
return { holdToken: token, expiresAt };
|
|
168
|
+
}
|
|
169
|
+
const result = await options.holds.place({
|
|
170
|
+
draftId: request.draftId,
|
|
171
|
+
productId: params.productId ?? request.entityId,
|
|
172
|
+
slotId,
|
|
173
|
+
paxCount,
|
|
174
|
+
ttlMs: request.ttlMs,
|
|
175
|
+
holdToken: token,
|
|
176
|
+
});
|
|
177
|
+
if (result.status === "ok") {
|
|
178
|
+
return { holdToken: result.holdToken, expiresAt: result.expiresAt };
|
|
179
|
+
}
|
|
180
|
+
// Capacity / lookup failures fall back to a stamping token
|
|
181
|
+
// — the journey commit will revalidate via the engine's
|
|
182
|
+
// re-quote and reject if capacity has dried up.
|
|
183
|
+
return { holdToken: token, expiresAt };
|
|
184
|
+
},
|
|
185
|
+
async extendHold(_ctx, holdToken, request) {
|
|
186
|
+
const ttlMs = request?.ttlMs ?? 30 * 60 * 1000;
|
|
187
|
+
if (options.holds) {
|
|
188
|
+
const result = await options.holds.extend({ holdToken, ttlMs });
|
|
189
|
+
if (result.status === "ok") {
|
|
190
|
+
return { holdToken, expiresAt: result.expiresAt };
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
return { holdToken, expiresAt: new Date(Date.now() + ttlMs) };
|
|
194
|
+
},
|
|
195
|
+
async releaseHold(_ctx, holdToken) {
|
|
196
|
+
if (options.holds) {
|
|
197
|
+
await options.holds.release(holdToken);
|
|
198
|
+
}
|
|
199
|
+
},
|
|
200
|
+
async commit(ctx, request) {
|
|
201
|
+
const draft = (request.draft ?? {});
|
|
202
|
+
// Defensive product load — the bridge will fail with
|
|
203
|
+
// `product_not_found` anyway, but a clean early-return keeps the
|
|
204
|
+
// commit path's error envelope predictable.
|
|
205
|
+
const product = await loadProduct(ctx.db, request.entityId);
|
|
206
|
+
if (!product) {
|
|
207
|
+
return {
|
|
208
|
+
status: "failed",
|
|
209
|
+
orderRef: "",
|
|
210
|
+
upstreamPayload: { reason: "product_not_found" },
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
const travelers = (draft.travelers ?? []).map((t) => ({
|
|
214
|
+
firstName: t.firstName,
|
|
215
|
+
lastName: t.lastName,
|
|
216
|
+
email: t.email,
|
|
217
|
+
phone: t.phone,
|
|
218
|
+
participantType: "traveler",
|
|
219
|
+
travelerCategory: t.band === "child" || t.band === "infant"
|
|
220
|
+
? t.band
|
|
221
|
+
: "adult",
|
|
222
|
+
}));
|
|
223
|
+
const bridge = await options.quickCreate({
|
|
224
|
+
productId: product.id,
|
|
225
|
+
bookingNumber: generateNumber(),
|
|
226
|
+
personId: extractPersonId(request.party),
|
|
227
|
+
organizationId: extractOrganizationId(request.party),
|
|
228
|
+
internalNotes: extractInternalNotes(request.party),
|
|
229
|
+
travelers: travelers.length > 0 ? travelers : undefined,
|
|
230
|
+
taxLines: extractTaxLines(request.pricing),
|
|
231
|
+
});
|
|
232
|
+
if (bridge.status !== "ok" || !bridge.bookingId) {
|
|
233
|
+
return {
|
|
234
|
+
status: "failed",
|
|
235
|
+
orderRef: "",
|
|
236
|
+
upstreamPayload: { bridge },
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
return {
|
|
240
|
+
status: "held",
|
|
241
|
+
orderRef: bridge.bookingNumber ?? bridge.bookingId,
|
|
242
|
+
pricing: request.pricing,
|
|
243
|
+
upstreamPayload: { bridgeBookingId: bridge.bookingId },
|
|
244
|
+
};
|
|
245
|
+
},
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
// ─────────────────────────────────────────────────────────────────
|
|
249
|
+
// Helpers
|
|
250
|
+
// ─────────────────────────────────────────────────────────────────
|
|
251
|
+
async function loadProduct(db, productId) {
|
|
252
|
+
const drizzle = db;
|
|
253
|
+
const rows = (await drizzle
|
|
254
|
+
.select()
|
|
255
|
+
.from(products)
|
|
256
|
+
.where(eq(products.id, productId))
|
|
257
|
+
.limit(1));
|
|
258
|
+
return rows[0];
|
|
259
|
+
}
|
|
260
|
+
function sumPax(pax) {
|
|
261
|
+
if (!pax)
|
|
262
|
+
return 0;
|
|
263
|
+
let total = 0;
|
|
264
|
+
for (const v of Object.values(pax)) {
|
|
265
|
+
if (typeof v === "number" && Number.isFinite(v) && v > 0)
|
|
266
|
+
total += v;
|
|
267
|
+
}
|
|
268
|
+
return total;
|
|
269
|
+
}
|
|
270
|
+
function extractPersonId(party) {
|
|
271
|
+
if (!party)
|
|
272
|
+
return undefined;
|
|
273
|
+
const v = party.personId;
|
|
274
|
+
return typeof v === "string" && v.length > 0 ? v : undefined;
|
|
275
|
+
}
|
|
276
|
+
function extractOrganizationId(party) {
|
|
277
|
+
if (!party)
|
|
278
|
+
return undefined;
|
|
279
|
+
const v = party.organizationId;
|
|
280
|
+
return typeof v === "string" && v.length > 0 ? v : undefined;
|
|
281
|
+
}
|
|
282
|
+
function extractInternalNotes(party) {
|
|
283
|
+
if (!party)
|
|
284
|
+
return undefined;
|
|
285
|
+
const v = party.internalNotes;
|
|
286
|
+
return typeof v === "string" && v.length > 0 ? v : undefined;
|
|
287
|
+
}
|
|
288
|
+
function extractTaxLines(pricing) {
|
|
289
|
+
const breakdown = pricing?.breakdown;
|
|
290
|
+
if (!breakdown || typeof breakdown !== "object" || Array.isArray(breakdown))
|
|
291
|
+
return undefined;
|
|
292
|
+
const taxes = breakdown.taxes;
|
|
293
|
+
if (!Array.isArray(taxes))
|
|
294
|
+
return undefined;
|
|
295
|
+
const lines = [];
|
|
296
|
+
for (const [index, tax] of taxes.entries()) {
|
|
297
|
+
if (!tax || typeof tax !== "object" || Array.isArray(tax))
|
|
298
|
+
continue;
|
|
299
|
+
const row = tax;
|
|
300
|
+
const amountCents = asFiniteInteger(row.amount);
|
|
301
|
+
const rate = typeof row.rate === "number" && Number.isFinite(row.rate) ? row.rate : null;
|
|
302
|
+
const currency = typeof pricing?.currency === "string" && pricing.currency.length === 3
|
|
303
|
+
? pricing.currency
|
|
304
|
+
: "EUR";
|
|
305
|
+
const name = typeof row.label === "string" && row.label.length > 0 ? row.label : "Tax";
|
|
306
|
+
if (!amountCents || amountCents <= 0)
|
|
307
|
+
continue;
|
|
308
|
+
const includedInPrice = row.includedInPrice === true || row.scope === "included";
|
|
309
|
+
lines.push({
|
|
310
|
+
code: typeof row.code === "string" ? row.code : null,
|
|
311
|
+
name,
|
|
312
|
+
scope: includedInPrice ? "included" : "excluded",
|
|
313
|
+
currency,
|
|
314
|
+
amountCents,
|
|
315
|
+
rateBasisPoints: rate == null ? null : Math.round(rate * 10_000),
|
|
316
|
+
includedInPrice,
|
|
317
|
+
sortOrder: index,
|
|
318
|
+
});
|
|
319
|
+
}
|
|
320
|
+
return lines.length ? lines : undefined;
|
|
321
|
+
}
|
|
322
|
+
function asFiniteInteger(value) {
|
|
323
|
+
if (typeof value !== "number" || !Number.isFinite(value))
|
|
324
|
+
return null;
|
|
325
|
+
return Math.round(value);
|
|
326
|
+
}
|
|
327
|
+
function defaultBookingNumber() {
|
|
328
|
+
const ts = Date.now().toString(36).toUpperCase();
|
|
329
|
+
return `BK-${ts}`;
|
|
330
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@voyantjs/products/booking-engine` — owned-arm booking handler
|
|
3
|
+
* for the products vertical.
|
|
4
|
+
*
|
|
5
|
+
* Per `docs/architecture/booking-journey-architecture.md` §6.
|
|
6
|
+
*/
|
|
7
|
+
export { type AvailabilityHoldBridge, buildOwnedProductDraftShape, type CreateProductsBookingHandlerOptions, createProductsBookingHandler, type QuickCreateBridge, type QuickCreateBridgeInput, type QuickCreateBridgeResult, } from "./handler.js";
|
|
8
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/booking-engine/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EACL,KAAK,sBAAsB,EAC3B,2BAA2B,EAC3B,KAAK,mCAAmC,EACxC,4BAA4B,EAC5B,KAAK,iBAAiB,EACtB,KAAK,sBAAsB,EAC3B,KAAK,uBAAuB,GAC7B,MAAM,cAAc,CAAA"}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@voyantjs/products/booking-engine` — owned-arm booking handler
|
|
3
|
+
* for the products vertical.
|
|
4
|
+
*
|
|
5
|
+
* Per `docs/architecture/booking-journey-architecture.md` §6.
|
|
6
|
+
*/
|
|
7
|
+
export { buildOwnedProductDraftShape, createProductsBookingHandler, } from "./handler.js";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"catalog-policy.d.ts","sourceRoot":"","sources":["../src/catalog-policy.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,OAAO,EAAqB,KAAK,gBAAgB,EAAE,MAAM,4BAA4B,CAAA;AAErF;;;GAGG;AACH,QAAA,MAAM,oBAAoB,EAAE,gBAAgB,
|
|
1
|
+
{"version":3,"file":"catalog-policy.d.ts","sourceRoot":"","sources":["../src/catalog-policy.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,OAAO,EAAqB,KAAK,gBAAgB,EAAE,MAAM,4BAA4B,CAAA;AAErF;;;GAGG;AACH,QAAA,MAAM,oBAAoB,EAAE,gBAAgB,EA0Y3C,CAAA;AAED;;;;GAIG;AACH,eAAO,MAAM,oBAAoB,2CAA0C,CAAA;AAE3E,OAAO,EAAE,oBAAoB,EAAE,CAAA"}
|
package/dist/catalog-policy.js
CHANGED
|
@@ -32,7 +32,7 @@ const PRODUCT_FIELD_POLICY = [
|
|
|
32
32
|
class: "managed",
|
|
33
33
|
merge: "source-only",
|
|
34
34
|
drift: "critical",
|
|
35
|
-
reindex: "
|
|
35
|
+
reindex: "facet-affecting",
|
|
36
36
|
snapshot: "on-book",
|
|
37
37
|
query: "indexed-column",
|
|
38
38
|
localized: false,
|
|
@@ -265,6 +265,20 @@ const PRODUCT_FIELD_POLICY = [
|
|
|
265
265
|
overrideFriction: "none",
|
|
266
266
|
sourceFreshness: "sync",
|
|
267
267
|
},
|
|
268
|
+
{
|
|
269
|
+
path: "supplierId",
|
|
270
|
+
class: "structural",
|
|
271
|
+
merge: "source-only",
|
|
272
|
+
drift: "high",
|
|
273
|
+
reindex: "facet-affecting",
|
|
274
|
+
snapshot: "on-book",
|
|
275
|
+
query: "indexed-column",
|
|
276
|
+
localized: false,
|
|
277
|
+
visibility: ["staff"],
|
|
278
|
+
editRole: "none",
|
|
279
|
+
overrideFriction: "none",
|
|
280
|
+
sourceFreshness: "sync",
|
|
281
|
+
},
|
|
268
282
|
{
|
|
269
283
|
path: "pax",
|
|
270
284
|
class: "structural",
|