@voyantjs/products 0.19.0 → 0.21.0

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.
Files changed (72) hide show
  1. package/dist/booking-engine/handler.d.ts +203 -0
  2. package/dist/booking-engine/handler.d.ts.map +1 -0
  3. package/dist/booking-engine/handler.js +330 -0
  4. package/dist/booking-engine/index.d.ts +8 -0
  5. package/dist/booking-engine/index.d.ts.map +1 -0
  6. package/dist/booking-engine/index.js +7 -0
  7. package/dist/catalog-policy.d.ts +33 -0
  8. package/dist/catalog-policy.d.ts.map +1 -0
  9. package/dist/catalog-policy.js +421 -0
  10. package/dist/content-shape.d.ts +217 -0
  11. package/dist/content-shape.d.ts.map +1 -0
  12. package/dist/content-shape.js +159 -0
  13. package/dist/draft-shape.d.ts +43 -0
  14. package/dist/draft-shape.d.ts.map +1 -0
  15. package/dist/draft-shape.js +46 -0
  16. package/dist/events.d.ts +37 -0
  17. package/dist/events.d.ts.map +1 -0
  18. package/dist/events.js +32 -0
  19. package/dist/index.d.ts +1 -0
  20. package/dist/index.d.ts.map +1 -1
  21. package/dist/index.js +1 -0
  22. package/dist/routes-content.d.ts +74 -0
  23. package/dist/routes-content.d.ts.map +1 -0
  24. package/dist/routes-content.js +117 -0
  25. package/dist/routes.d.ts +47 -26
  26. package/dist/routes.d.ts.map +1 -1
  27. package/dist/routes.js +88 -16
  28. package/dist/schema-core.d.ts +240 -1
  29. package/dist/schema-core.d.ts.map +1 -1
  30. package/dist/schema-core.js +49 -0
  31. package/dist/schema-itinerary.d.ts +18 -1
  32. package/dist/schema-itinerary.d.ts.map +1 -1
  33. package/dist/schema-itinerary.js +1 -0
  34. package/dist/schema-settings.d.ts +1 -1
  35. package/dist/schema-sourced-content.d.ts +262 -0
  36. package/dist/schema-sourced-content.d.ts.map +1 -0
  37. package/dist/schema-sourced-content.js +69 -0
  38. package/dist/schema-taxonomy.d.ts +17 -0
  39. package/dist/schema-taxonomy.d.ts.map +1 -1
  40. package/dist/schema-taxonomy.js +13 -0
  41. package/dist/schema.d.ts +1 -0
  42. package/dist/schema.d.ts.map +1 -1
  43. package/dist/schema.js +1 -0
  44. package/dist/service-catalog-plane.d.ts +129 -0
  45. package/dist/service-catalog-plane.d.ts.map +1 -0
  46. package/dist/service-catalog-plane.js +212 -0
  47. package/dist/service-content-owned.d.ts +68 -0
  48. package/dist/service-content-owned.d.ts.map +1 -0
  49. package/dist/service-content-owned.js +224 -0
  50. package/dist/service-content-synthesizer.d.ts +90 -0
  51. package/dist/service-content-synthesizer.d.ts.map +1 -0
  52. package/dist/service-content-synthesizer.js +171 -0
  53. package/dist/service-content.d.ts +106 -0
  54. package/dist/service-content.d.ts.map +1 -0
  55. package/dist/service-content.js +365 -0
  56. package/dist/service.d.ts +82 -28
  57. package/dist/service.d.ts.map +1 -1
  58. package/dist/service.js +4 -0
  59. package/dist/tasks/brochures.d.ts +2 -1
  60. package/dist/tasks/brochures.d.ts.map +1 -1
  61. package/dist/tasks/brochures.js +3 -0
  62. package/dist/validation-catalog.d.ts +4 -4
  63. package/dist/validation-config.d.ts +3 -3
  64. package/dist/validation-content.d.ts +34 -4
  65. package/dist/validation-content.d.ts.map +1 -1
  66. package/dist/validation-content.js +13 -0
  67. package/dist/validation-core.d.ts +53 -3
  68. package/dist/validation-core.d.ts.map +1 -1
  69. package/dist/validation-core.js +16 -0
  70. package/dist/validation-public.d.ts +9 -9
  71. package/dist/validation-shared.d.ts +4 -4
  72. package/package.json +12 -6
@@ -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";
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Catalog plane field policy for `packages/products`.
3
+ *
4
+ * Declares every product field's governance under the
5
+ * `@voyantjs/catalog` 12-attribute contract. Phase B shake-out
6
+ * adoption — see `docs/architecture/catalog-architecture.md` §9.1.
7
+ *
8
+ * Scope of this file:
9
+ * - The root `products` table (from `schema-core.ts`).
10
+ * - Provenance + identity fields the catalog plane needs to track.
11
+ *
12
+ * Out of scope (deferred to follow-up adoption passes):
13
+ * - `productOptions`, `optionUnits`, `productDays`, `productNotes`,
14
+ * `productVersions` — promoted child entities (per composition rule §6.2);
15
+ * each gets its own micro-registry when wired in.
16
+ * - The split of `tags` into `marketing_tags` + `facet_tags` per the
17
+ * human-readable / machine-evaluable rule (§7.1). Today's schema has a
18
+ * single `tags` column; declared here as merchandisable with a TODO.
19
+ */
20
+ import { type FieldPolicyInput } from "@voyantjs/catalog/contract";
21
+ /**
22
+ * Field-policy declarations for `products`. Pass through `defineFieldPolicy`
23
+ * to apply inheritance and produce the runtime registry.
24
+ */
25
+ declare const PRODUCT_FIELD_POLICY: FieldPolicyInput[];
26
+ /**
27
+ * Resolved field-policy registry for products. Verticals adopt the catalog
28
+ * plane by exporting this; templates wire it into the indexer, overlay
29
+ * resolver, and snapshot capture pipeline.
30
+ */
31
+ export declare const productCatalogPolicy: import("@voyantjs/catalog").FieldPolicy[];
32
+ export { PRODUCT_FIELD_POLICY };
33
+ //# sourceMappingURL=catalog-policy.d.ts.map
@@ -0,0 +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,EA0Y3C,CAAA;AAED;;;;GAIG;AACH,eAAO,MAAM,oBAAoB,2CAA0C,CAAA;AAE3E,OAAO,EAAE,oBAAoB,EAAE,CAAA"}