@voyantjs/products 0.27.0 → 0.28.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.
@@ -96,6 +96,25 @@ export interface BuildOwnedProductDraftShapeOptions {
96
96
  addonCatalog?: ReadonlyArray<AddonOffer>;
97
97
  }
98
98
  export declare function buildOwnedProductDraftShape(options?: BuildOwnedProductDraftShapeOptions): BookingDraftShape;
99
+ /** A per-unit price within a resolved option price rule, returned by
100
+ * `loadResolvedOptionPrice`. The handler matches `travelerCategory`
101
+ * to the draft's pax-band codes ("adult" / "child" / "infant" /
102
+ * "senior") to compute per-band totals. Units that don't map to a
103
+ * band — or whose band has zero count — are dropped. */
104
+ export interface ResolvedUnitPrice {
105
+ unitId: string;
106
+ unitType: "person" | "room" | "vehicle" | "service" | "group" | "other" | string;
107
+ travelerCategory: "adult" | "child" | "infant" | "senior" | null;
108
+ sellAmountCents: number | null;
109
+ }
110
+ /** Output of `loadResolvedOptionPrice`. The handler prefers
111
+ * `unitPrices` (per-band pricing) when present and any unit matches a
112
+ * pax band; otherwise falls back to `baseSellAmountCents × paxCount`
113
+ * for per-booking rules; otherwise back to `product.sellAmountCents`. */
114
+ export interface ResolvedOptionPrice {
115
+ baseSellAmountCents: number | null;
116
+ unitPrices: ReadonlyArray<ResolvedUnitPrice>;
117
+ }
99
118
  /** A resolved tax-rate decision — resolved from `tax_classes` ×
100
119
  * `tax_regimes` × buyer country at quote time. */
101
120
  export interface ResolvedTaxRate {
@@ -139,6 +158,33 @@ export interface OwnedProductsShapeLoaders {
139
158
  buyerCountry?: string;
140
159
  buyerType?: "B2C" | "B2B";
141
160
  }) => Promise<ResolvedTaxRate | null>;
161
+ /**
162
+ * Resolve the option price rule for a given (product, option, date)
163
+ * — typically backed by `@voyantjs/pricing`'s
164
+ * `resolveOptionPriceRulesForDate` plus a join into per-unit prices.
165
+ * Returns null when no rule applies or the resolver can't run; the
166
+ * handler then falls back to `product.sellAmountCents × pax`.
167
+ *
168
+ * Caller-supplied so `@voyantjs/products` does not import
169
+ * `@voyantjs/pricing` (the dependency direction is pricing →
170
+ * products, not the reverse).
171
+ */
172
+ loadResolvedOptionPrice?: (ctx: OwnedHandlerContext, args: {
173
+ productId: string;
174
+ optionId: string;
175
+ /** ISO yyyy-mm-dd in the slot's local timezone. */
176
+ date: string;
177
+ catalogId?: string;
178
+ }) => Promise<ResolvedOptionPrice | null>;
179
+ /**
180
+ * Look up the local date of a departure slot (`availability_slots`).
181
+ * Caller-supplied so the products package does not import
182
+ * `@voyantjs/availability`. Returns null when the slot is missing.
183
+ *
184
+ * Used together with `loadResolvedOptionPrice` to convert a draft's
185
+ * `departureSlotId` into a date the resolver can match against.
186
+ */
187
+ loadSlotDate?: (ctx: OwnedHandlerContext, slotId: string) => Promise<string | null>;
142
188
  }
143
189
  /**
144
190
  * Caller-supplied availability-hold bridge — keeps the products
@@ -1 +1 @@
1
- {"version":3,"file":"handler.d.ts","sourceRoot":"","sources":["../../src/booking-engine/handler.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"handler.d.ts","sourceRoot":"","sources":["../../src/booking-engine/handler.ts"],"names":[],"mappings":"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"}
@@ -60,9 +60,12 @@ export function createProductsBookingHandler(options) {
60
60
  };
61
61
  }
62
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([
63
+ const optionId = draft.configure?.variantId;
64
+ const slotId = draft.configure?.departureSlotId;
65
+ // Concurrent enrichment + slot-date lookup. The slot date is
66
+ // needed before we can call loadResolvedOptionPrice, so it
67
+ // joins this batch.
68
+ const [travelerFields, addonCatalog, taxRate, slotDate] = await Promise.all([
66
69
  options.loadTravelerFields?.(ctx, request.entityId) ?? Promise.resolve(undefined),
67
70
  options.loadAddonCatalog?.(ctx, request.entityId) ?? Promise.resolve(undefined),
68
71
  options.loadTaxRate?.(ctx, {
@@ -70,19 +73,34 @@ export function createProductsBookingHandler(options) {
70
73
  buyerCountry: draft.billing?.address?.country,
71
74
  buyerType: draft.billing?.buyerType,
72
75
  }) ?? Promise.resolve(null),
76
+ slotId && options.loadSlotDate
77
+ ? options.loadSlotDate(ctx, slotId)
78
+ : Promise.resolve(draft.configure?.departureDate ?? null),
73
79
  ]);
80
+ const resolvedPrice = optionId && slotDate && options.loadResolvedOptionPrice
81
+ ? await options.loadResolvedOptionPrice(ctx, {
82
+ productId: request.entityId,
83
+ optionId,
84
+ date: slotDate,
85
+ })
86
+ : null;
74
87
  const paxCount = sumPax(draft.configure?.pax);
75
88
  // Per-pax pricing fallback: when no pax is supplied yet, quote a
76
89
  // single-occupant baseline so the wizard can render a starter
77
90
  // total before the user picks counts.
78
91
  const effectivePax = paxCount > 0 ? paxCount : 1;
79
- const unitCents = product.sellAmountCents ?? 0;
80
- const grossCents = unitCents * effectivePax;
92
+ const priced = priceQuote({
93
+ product,
94
+ resolvedPrice,
95
+ pax: draft.configure?.pax,
96
+ effectivePax,
97
+ });
81
98
  // Tax computation. The base is taxable; addons/accommodation
82
99
  // get the same rate in this MVP cut. Per-line override (the
83
100
  // `applies_to` axis on tax_classes.lines) lands in a follow-up
84
101
  // when the catalog actually carries mixed treatments.
85
102
  const taxIsInclusive = taxRate?.priceMode === "inclusive";
103
+ const grossCents = priced.totalCents;
86
104
  const taxCents = taxRate && taxRate.rate > 0
87
105
  ? taxIsInclusive
88
106
  ? Math.round(grossCents - grossCents / (1 + taxRate.rate))
@@ -90,7 +108,8 @@ export function createProductsBookingHandler(options) {
90
108
  : 0;
91
109
  const netCents = taxIsInclusive ? grossCents - taxCents : grossCents;
92
110
  const payableCents = taxIsInclusive ? grossCents : netCents + taxCents;
93
- const pricing = unitCents > 0
111
+ const available = grossCents > 0;
112
+ const pricing = available
94
113
  ? {
95
114
  base_amount: netCents,
96
115
  taxes: taxCents,
@@ -98,16 +117,10 @@ export function createProductsBookingHandler(options) {
98
117
  surcharges: 0,
99
118
  currency: product.sellCurrency,
100
119
  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
- ],
120
+ lines: priced.lines.map((line) => ({
121
+ ...line,
122
+ taxIncluded: taxIsInclusive,
123
+ })),
111
124
  taxes: taxRate && taxCents > 0
112
125
  ? [
113
126
  {
@@ -129,8 +142,8 @@ export function createProductsBookingHandler(options) {
129
142
  }
130
143
  : undefined;
131
144
  return {
132
- available: unitCents > 0,
133
- invalidReason: unitCents > 0 ? undefined : "no_sell_amount_configured",
145
+ available,
146
+ invalidReason: available ? undefined : "no_sell_amount_configured",
134
147
  pricing,
135
148
  shape: buildOwnedProductDraftShape({
136
149
  travelerFields,
@@ -267,6 +280,80 @@ function sumPax(pax) {
267
280
  }
268
281
  return total;
269
282
  }
283
+ /**
284
+ * Three-way price computation:
285
+ *
286
+ * 1. **Per-band** (preferred): when `resolvedPrice.unitPrices` matches
287
+ * at least one band with positive count, sum `pax[band] ×
288
+ * unit.sellAmountCents` for each matching band. One breakdown line
289
+ * per band.
290
+ *
291
+ * 2. **Per-booking**: when no per-band match but `baseSellAmountCents`
292
+ * is set, charge a single `base × paxCount` line.
293
+ *
294
+ * 3. **Fallback**: `product.sellAmountCents × paxCount`. Same shape as
295
+ * Phase A behavior, kept for bookings without an option/slot
296
+ * configured yet.
297
+ */
298
+ function priceQuote(input) {
299
+ const { product, resolvedPrice, pax, effectivePax } = input;
300
+ if (resolvedPrice && resolvedPrice.unitPrices.length > 0) {
301
+ const bandLines = [];
302
+ let total = 0;
303
+ for (const unit of resolvedPrice.unitPrices) {
304
+ if (!unit.travelerCategory)
305
+ continue;
306
+ const count = pax?.[unit.travelerCategory] ?? 0;
307
+ if (count <= 0)
308
+ continue;
309
+ const sell = unit.sellAmountCents ?? 0;
310
+ if (sell <= 0)
311
+ continue;
312
+ const lineTotal = sell * count;
313
+ total += lineTotal;
314
+ bandLines.push({
315
+ kind: "base",
316
+ label: `${product.name} — ${unit.travelerCategory}`,
317
+ quantity: count,
318
+ unitAmount: sell,
319
+ totalAmount: lineTotal,
320
+ });
321
+ }
322
+ if (bandLines.length > 0) {
323
+ return { totalCents: total, lines: bandLines };
324
+ }
325
+ }
326
+ if (resolvedPrice && resolvedPrice.baseSellAmountCents !== null) {
327
+ const unitCents = resolvedPrice.baseSellAmountCents;
328
+ const totalCents = unitCents * effectivePax;
329
+ return {
330
+ totalCents,
331
+ lines: [
332
+ {
333
+ kind: "base",
334
+ label: product.name,
335
+ quantity: effectivePax,
336
+ unitAmount: unitCents,
337
+ totalAmount: totalCents,
338
+ },
339
+ ],
340
+ };
341
+ }
342
+ const unitCents = product.sellAmountCents ?? 0;
343
+ const totalCents = unitCents * effectivePax;
344
+ return {
345
+ totalCents,
346
+ lines: [
347
+ {
348
+ kind: "base",
349
+ label: product.name,
350
+ quantity: effectivePax,
351
+ unitAmount: unitCents,
352
+ totalAmount: totalCents,
353
+ },
354
+ ],
355
+ };
356
+ }
270
357
  function extractPersonId(party) {
271
358
  if (!party)
272
359
  return undefined;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@voyantjs/products",
3
- "version": "0.27.0",
3
+ "version": "0.28.0",
4
4
  "license": "Apache-2.0",
5
5
  "type": "module",
6
6
  "exports": {
@@ -55,12 +55,12 @@
55
55
  "hono": "^4.12.10",
56
56
  "pdf-lib": "^1.17.1",
57
57
  "zod": "^4.3.6",
58
- "@voyantjs/core": "0.27.0",
59
- "@voyantjs/db": "0.27.0",
60
- "@voyantjs/hono": "0.27.0",
61
- "@voyantjs/utils": "0.27.0",
62
- "@voyantjs/catalog": "0.27.0",
63
- "@voyantjs/storage": "0.27.0"
58
+ "@voyantjs/core": "0.28.0",
59
+ "@voyantjs/db": "0.28.0",
60
+ "@voyantjs/hono": "0.28.0",
61
+ "@voyantjs/utils": "0.28.0",
62
+ "@voyantjs/catalog": "0.28.0",
63
+ "@voyantjs/storage": "0.28.0"
64
64
  },
65
65
  "devDependencies": {
66
66
  "typescript": "^6.0.2",