@voyantjs/pricing 0.28.1 → 0.29.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.
@@ -0,0 +1,53 @@
1
+ /**
2
+ * Pricing domain events.
3
+ *
4
+ * Emitted (after commit) by paths that mutate pricing rules feeding the
5
+ * catalog-plane `priceFromAmountCents` projection (PR4 of #493). The
6
+ * catalog bridge subscribes to these and reindexes the affected
7
+ * product so the denormalized "from $X" stays in sync without a full
8
+ * reindex run.
9
+ *
10
+ * Subscriber contract: treat the payload as a *trigger*, not the source
11
+ * of truth. Re-read the row(s) before acting on them — concurrent edits
12
+ * may have superseded the snapshot in this payload.
13
+ *
14
+ * Per docs/architecture/channel-push-architecture.md §5.1 (same shape as
15
+ * `availability.slot.changed`).
16
+ *
17
+ * Today's surface covers `option_price_rules` + `option_unit_price_rules`
18
+ * — the two tables that feed PR4's MIN aggregation. Other pricing
19
+ * tables (`option_start_time_rules`, `option_unit_tiers`,
20
+ * `departure_price_overrides`, `pickup_price_rules`, `cancellation_*`)
21
+ * intentionally do NOT emit yet: extending the catalog projection to
22
+ * read them is its own design conversation and emitting an event no
23
+ * subscriber consumes is dead surface area. When a future projection
24
+ * grows to read one of those tables, extend `kind` and add emission to
25
+ * the corresponding service function.
26
+ */
27
+ /** Stable string identifier for the event. */
28
+ export declare const PRICING_RULE_CHANGED_EVENT: "pricing.rule.changed";
29
+ /** Origin of the change. Diagnostic only — subscribers don't behave
30
+ * differently on this field, but it's preserved end-to-end so logs
31
+ * and dashboards can attribute drift to a cause. */
32
+ export type PricingRuleChangeSource = "created" | "updated" | "deleted";
33
+ /**
34
+ * Which pricing-rule table the mutation touched. Lets a subscriber
35
+ * filter without re-reading the row (e.g. a future projection that
36
+ * only cares about `option-rule` mutations can skip per-unit deltas).
37
+ */
38
+ export type PricingRuleKind = "option-rule" | "option-unit-rule";
39
+ export interface PricingRuleChangedEvent {
40
+ /**
41
+ * The product whose pricing surface changed. Subscribers reindex
42
+ * this product. Always set — every relevant rule belongs to a
43
+ * product (option-unit rules join through their parent option-rule
44
+ * to find the product).
45
+ */
46
+ productId: string;
47
+ /** The mutated row's id, for diagnostics. */
48
+ ruleId: string;
49
+ /** Which table was touched. */
50
+ kind: PricingRuleKind;
51
+ source: PricingRuleChangeSource;
52
+ }
53
+ //# sourceMappingURL=events.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"events.d.ts","sourceRoot":"","sources":["../src/events.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AAEH,8CAA8C;AAC9C,eAAO,MAAM,0BAA0B,EAAG,sBAA+B,CAAA;AAEzE;;qDAEqD;AACrD,MAAM,MAAM,uBAAuB,GAAG,SAAS,GAAG,SAAS,GAAG,SAAS,CAAA;AAEvE;;;;GAIG;AACH,MAAM,MAAM,eAAe,GAAG,aAAa,GAAG,kBAAkB,CAAA;AAEhE,MAAM,WAAW,uBAAuB;IACtC;;;;;OAKG;IACH,SAAS,EAAE,MAAM,CAAA;IACjB,6CAA6C;IAC7C,MAAM,EAAE,MAAM,CAAA;IACd,+BAA+B;IAC/B,IAAI,EAAE,eAAe,CAAA;IACrB,MAAM,EAAE,uBAAuB,CAAA;CAChC"}
package/dist/events.js ADDED
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Pricing domain events.
3
+ *
4
+ * Emitted (after commit) by paths that mutate pricing rules feeding the
5
+ * catalog-plane `priceFromAmountCents` projection (PR4 of #493). The
6
+ * catalog bridge subscribes to these and reindexes the affected
7
+ * product so the denormalized "from $X" stays in sync without a full
8
+ * reindex run.
9
+ *
10
+ * Subscriber contract: treat the payload as a *trigger*, not the source
11
+ * of truth. Re-read the row(s) before acting on them — concurrent edits
12
+ * may have superseded the snapshot in this payload.
13
+ *
14
+ * Per docs/architecture/channel-push-architecture.md §5.1 (same shape as
15
+ * `availability.slot.changed`).
16
+ *
17
+ * Today's surface covers `option_price_rules` + `option_unit_price_rules`
18
+ * — the two tables that feed PR4's MIN aggregation. Other pricing
19
+ * tables (`option_start_time_rules`, `option_unit_tiers`,
20
+ * `departure_price_overrides`, `pickup_price_rules`, `cancellation_*`)
21
+ * intentionally do NOT emit yet: extending the catalog projection to
22
+ * read them is its own design conversation and emitting an event no
23
+ * subscriber consumes is dead surface area. When a future projection
24
+ * grows to read one of those tables, extend `kind` and add emission to
25
+ * the corresponding service function.
26
+ */
27
+ /** Stable string identifier for the event. */
28
+ export const PRICING_RULE_CHANGED_EVENT = "pricing.rule.changed";
@@ -11,7 +11,7 @@ export declare const pricingCoreRoutes: import("hono/hono-base").HonoBase<Env, {
11
11
  unitId: string | null;
12
12
  code: string | null;
13
13
  name: string;
14
- categoryType: "service" | "other" | "adult" | "child" | "infant" | "senior" | "group" | "room" | "vehicle";
14
+ categoryType: "service" | "child" | "other" | "adult" | "infant" | "senior" | "group" | "room" | "vehicle";
15
15
  seatOccupancy: number;
16
16
  groupSize: number | null;
17
17
  isAgeQualified: boolean;
@@ -43,8 +43,8 @@ export declare const pricingCoreRoutes: import("hono/hono-base").HonoBase<Env, {
43
43
  metadata: {
44
44
  [x: string]: import("hono/utils/types").JSONValue;
45
45
  } | null;
46
- id: string;
47
46
  name: string;
47
+ id: string;
48
48
  createdAt: string;
49
49
  updatedAt: string;
50
50
  code: string | null;
@@ -52,7 +52,7 @@ export declare const pricingCoreRoutes: import("hono/hono-base").HonoBase<Env, {
52
52
  productId: string | null;
53
53
  optionId: string | null;
54
54
  unitId: string | null;
55
- categoryType: "service" | "other" | "adult" | "child" | "infant" | "senior" | "group" | "room" | "vehicle";
55
+ categoryType: "service" | "child" | "other" | "adult" | "infant" | "senior" | "group" | "room" | "vehicle";
56
56
  seatOccupancy: number;
57
57
  groupSize: number | null;
58
58
  isAgeQualified: boolean;
@@ -93,7 +93,7 @@ export declare const pricingCoreRoutes: import("hono/hono-base").HonoBase<Env, {
93
93
  unitId: string | null;
94
94
  code: string | null;
95
95
  name: string;
96
- categoryType: "service" | "other" | "adult" | "child" | "infant" | "senior" | "group" | "room" | "vehicle";
96
+ categoryType: "service" | "child" | "other" | "adult" | "infant" | "senior" | "group" | "room" | "vehicle";
97
97
  seatOccupancy: number;
98
98
  groupSize: number | null;
99
99
  isAgeQualified: boolean;
@@ -140,7 +140,7 @@ export declare const pricingCoreRoutes: import("hono/hono-base").HonoBase<Env, {
140
140
  unitId: string | null;
141
141
  code: string | null;
142
142
  name: string;
143
- categoryType: "service" | "other" | "adult" | "child" | "infant" | "senior" | "group" | "room" | "vehicle";
143
+ categoryType: "service" | "child" | "other" | "adult" | "infant" | "senior" | "group" | "room" | "vehicle";
144
144
  seatOccupancy: number;
145
145
  groupSize: number | null;
146
146
  isAgeQualified: boolean;
@@ -370,8 +370,8 @@ export declare const pricingCoreRoutes: import("hono/hono-base").HonoBase<Env, {
370
370
  metadata: {
371
371
  [x: string]: import("hono/utils/types").JSONValue;
372
372
  } | null;
373
- id: string;
374
373
  name: string;
374
+ id: string;
375
375
  createdAt: string;
376
376
  updatedAt: string;
377
377
  notes: string | null;
@@ -680,8 +680,8 @@ export declare const pricingCoreRoutes: import("hono/hono-base").HonoBase<Env, {
680
680
  metadata: {
681
681
  [x: string]: import("hono/utils/types").JSONValue;
682
682
  } | null;
683
- id: string;
684
683
  name: string;
684
+ id: string;
685
685
  createdAt: string;
686
686
  updatedAt: string;
687
687
  notes: string | null;
@@ -843,8 +843,8 @@ export declare const pricingCoreRoutes: import("hono/hono-base").HonoBase<Env, {
843
843
  metadata: {
844
844
  [x: string]: import("hono/utils/types").JSONValue;
845
845
  } | null;
846
- id: string;
847
846
  name: string;
847
+ id: string;
848
848
  createdAt: string;
849
849
  updatedAt: string;
850
850
  notes: string | null;
@@ -110,7 +110,7 @@ export declare const publicPricingRoutes: import("hono/hono-base").HonoBase<Env,
110
110
  startsAt: string | null;
111
111
  endsAt: string | null;
112
112
  timezone: string;
113
- status: "open" | "closed" | "sold_out" | "cancelled";
113
+ status: "cancelled" | "open" | "closed" | "sold_out";
114
114
  unlimited: boolean;
115
115
  remainingPax: number | null;
116
116
  remainingResources: number | null;
@@ -46,8 +46,8 @@ export declare const pricingRuleRoutes: import("hono/hono-base").HonoBase<Env, {
46
46
  metadata: {
47
47
  [x: string]: import("hono/utils/types").JSONValue;
48
48
  } | null;
49
- id: string;
50
49
  name: string;
50
+ id: string;
51
51
  createdAt: string;
52
52
  updatedAt: string;
53
53
  notes: string | null;
@@ -1 +1 @@
1
- {"version":3,"file":"routes-rules.d.ts","sourceRoot":"","sources":["../src/routes-rules.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,KAAK,GAAG,EAAY,MAAM,oBAAoB,CAAA;AA6BvD,eAAO,MAAM,iBAAiB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;yCAwR1B,CAAA"}
1
+ {"version":3,"file":"routes-rules.d.ts","sourceRoot":"","sources":["../src/routes-rules.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,KAAK,GAAG,EAAY,MAAM,oBAAoB,CAAA;AA6BvD,eAAO,MAAM,iBAAiB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;yCAgS1B,CAAA"}
@@ -6,34 +6,38 @@ import { departurePriceOverrideListQuerySchema, dropoffPriceRuleListQuerySchema,
6
6
  export const pricingRuleRoutes = new Hono()
7
7
  .get("/option-price-rules", async (c) => c.json(await pricingService.listOptionPriceRules(c.get("db"), await parseQuery(c, optionPriceRuleListQuerySchema))))
8
8
  .post("/option-price-rules", async (c) => c.json({
9
- data: await pricingService.createOptionPriceRule(c.get("db"), await parseJsonBody(c, insertOptionPriceRuleSchema)),
9
+ data: await pricingService.createOptionPriceRule(c.get("db"), await parseJsonBody(c, insertOptionPriceRuleSchema), { eventBus: c.get("eventBus") }),
10
10
  }, 201))
11
11
  .get("/option-price-rules/:id", async (c) => {
12
12
  const row = await pricingService.getOptionPriceRuleById(c.get("db"), c.req.param("id"));
13
13
  return row ? c.json({ data: row }) : notFound(c, "Option price rule not found");
14
14
  })
15
15
  .patch("/option-price-rules/:id", async (c) => {
16
- const row = await pricingService.updateOptionPriceRule(c.get("db"), c.req.param("id"), await parseJsonBody(c, updateOptionPriceRuleSchema));
16
+ const row = await pricingService.updateOptionPriceRule(c.get("db"), c.req.param("id"), await parseJsonBody(c, updateOptionPriceRuleSchema), { eventBus: c.get("eventBus") });
17
17
  return row ? c.json({ data: row }) : notFound(c, "Option price rule not found");
18
18
  })
19
19
  .delete("/option-price-rules/:id", async (c) => {
20
- const row = await pricingService.deleteOptionPriceRule(c.get("db"), c.req.param("id"));
20
+ const row = await pricingService.deleteOptionPriceRule(c.get("db"), c.req.param("id"), {
21
+ eventBus: c.get("eventBus"),
22
+ });
21
23
  return row ? c.json({ success: true }) : notFound(c, "Option price rule not found");
22
24
  })
23
25
  .get("/option-unit-price-rules", async (c) => c.json(await pricingService.listOptionUnitPriceRules(c.get("db"), await parseQuery(c, optionUnitPriceRuleListQuerySchema))))
24
26
  .post("/option-unit-price-rules", async (c) => c.json({
25
- data: await pricingService.createOptionUnitPriceRule(c.get("db"), await parseJsonBody(c, insertOptionUnitPriceRuleSchema)),
27
+ data: await pricingService.createOptionUnitPriceRule(c.get("db"), await parseJsonBody(c, insertOptionUnitPriceRuleSchema), { eventBus: c.get("eventBus") }),
26
28
  }, 201))
27
29
  .get("/option-unit-price-rules/:id", async (c) => {
28
30
  const row = await pricingService.getOptionUnitPriceRuleById(c.get("db"), c.req.param("id"));
29
31
  return row ? c.json({ data: row }) : notFound(c, "Option unit price rule not found");
30
32
  })
31
33
  .patch("/option-unit-price-rules/:id", async (c) => {
32
- const row = await pricingService.updateOptionUnitPriceRule(c.get("db"), c.req.param("id"), await parseJsonBody(c, updateOptionUnitPriceRuleSchema));
34
+ const row = await pricingService.updateOptionUnitPriceRule(c.get("db"), c.req.param("id"), await parseJsonBody(c, updateOptionUnitPriceRuleSchema), { eventBus: c.get("eventBus") });
33
35
  return row ? c.json({ data: row }) : notFound(c, "Option unit price rule not found");
34
36
  })
35
37
  .delete("/option-unit-price-rules/:id", async (c) => {
36
- const row = await pricingService.deleteOptionUnitPriceRule(c.get("db"), c.req.param("id"));
38
+ const row = await pricingService.deleteOptionUnitPriceRule(c.get("db"), c.req.param("id"), {
39
+ eventBus: c.get("eventBus"),
40
+ });
37
41
  return row ? c.json({ success: true }) : notFound(c, "Option unit price rule not found");
38
42
  })
39
43
  .get("/option-start-time-rules", async (c) => c.json(await pricingService.listOptionStartTimeRules(c.get("db"), await parseQuery(c, optionStartTimeRuleListQuerySchema))))
@@ -1,9 +1,11 @@
1
+ import type { EventBus } from "@voyantjs/core";
1
2
  import type { PostgresJsDatabase } from "drizzle-orm/postgres-js";
2
3
  import type { Context } from "hono";
3
4
  export type Env = {
4
5
  Variables: {
5
6
  db: PostgresJsDatabase;
6
7
  userId?: string;
8
+ eventBus?: EventBus;
7
9
  };
8
10
  };
9
11
  export declare function notFound(c: Context<Env>, message: string): Response & import("hono").TypedResponse<{
@@ -1 +1 @@
1
- {"version":3,"file":"routes-shared.d.ts","sourceRoot":"","sources":["../src/routes-shared.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,yBAAyB,CAAA;AACjE,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,MAAM,CAAA;AAEnC,MAAM,MAAM,GAAG,GAAG;IAChB,SAAS,EAAE;QACT,EAAE,EAAE,kBAAkB,CAAA;QACtB,MAAM,CAAC,EAAE,MAAM,CAAA;KAChB,CAAA;CACF,CAAA;AAED,wBAAgB,QAAQ,CAAC,CAAC,EAAE,OAAO,CAAC,GAAG,CAAC,EAAE,OAAO,EAAE,MAAM;;gBAExD"}
1
+ {"version":3,"file":"routes-shared.d.ts","sourceRoot":"","sources":["../src/routes-shared.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAA;AAC9C,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,yBAAyB,CAAA;AACjE,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,MAAM,CAAA;AAEnC,MAAM,MAAM,GAAG,GAAG;IAChB,SAAS,EAAE;QACT,EAAE,EAAE,kBAAkB,CAAA;QACtB,MAAM,CAAC,EAAE,MAAM,CAAA;QACf,QAAQ,CAAC,EAAE,QAAQ,CAAA;KACpB,CAAA;CACF,CAAA;AAED,wBAAgB,QAAQ,CAAC,CAAC,EAAE,OAAO,CAAC,GAAG,CAAC,EAAE,OAAO,EAAE,MAAM;;gBAExD"}
@@ -109,7 +109,7 @@ export declare const pricingCategories: import("drizzle-orm/pg-core").PgTableWit
109
109
  tableName: "pricing_categories";
110
110
  dataType: "string";
111
111
  columnType: "PgEnumColumn";
112
- data: "service" | "other" | "adult" | "child" | "infant" | "senior" | "group" | "room" | "vehicle";
112
+ data: "service" | "child" | "other" | "adult" | "infant" | "senior" | "group" | "room" | "vehicle";
113
113
  driverParam: string;
114
114
  notNull: true;
115
115
  hasDefault: true;
@@ -0,0 +1,80 @@
1
+ /**
2
+ * Projection extension that aggregates a "price from" amount across the
3
+ * product's configured default-rule prices and contributes
4
+ * `priceFromAmountCents`, `priceFromCurrency`, and `hasPricing` to the
5
+ * product search document.
6
+ *
7
+ * Lives in `@voyantjs/pricing` because:
8
+ * - The data lives here (`option_price_rules`, `option_unit_price_rules`,
9
+ * `price_catalogs`).
10
+ * - `pricing` already depends on `products`; importing the
11
+ * `ProductProjectionExtension` contract type from products is the
12
+ * same direction. The reverse would create a circular dep.
13
+ *
14
+ * Wire via `createProductDocumentBuilder({ extensions: [pricingExtension] })`
15
+ * after composing `productPricingCatalogPolicy` into the registry.
16
+ *
17
+ * Scope intentionally narrow:
18
+ * - **No schedule-aware rule resolution.** Only `is_default = true`
19
+ * rules contribute. Seasonal / promo rules with schedules don't
20
+ * surface here — they'd require per-slice resolution against a
21
+ * moving "now" date and are deferred to a follow-up.
22
+ * - **No per-departure overrides.** Same reason.
23
+ * - **Currency consistency.** Only rules whose catalog currency matches
24
+ * the product's `sellCurrency` (or whose catalog currency is null
25
+ * and therefore inherits the product's) are MIN'd together. This
26
+ * prevents emitting a misleading "from $50" when one of the rules
27
+ * is actually €50.
28
+ *
29
+ * Document churn: this projection is `now()`-independent — it reads
30
+ * static configured prices, not date-dependent rule windows. Same
31
+ * product reindexed an hour later produces the same fields.
32
+ */
33
+ import type { AnyDrizzleDb } from "@voyantjs/db";
34
+ import type { ProductProjectionExtension } from "@voyantjs/products/service-catalog-plane";
35
+ interface PricingProjectionOptions {
36
+ /**
37
+ * Resolve the product's `sellAmountCents` + `sellCurrency` so the
38
+ * projection can MIN the product-row default into the candidate set
39
+ * and filter rules by matching currency. Templates inject this — the
40
+ * default reads the products table via raw SQL, but tests can stub
41
+ * with a known shape without standing up the products schema.
42
+ *
43
+ * Returns `null` for both fields when the product doesn't exist.
44
+ */
45
+ loadProductPricing?: (db: AnyDrizzleDb, productId: string) => Promise<{
46
+ sellAmountCents: number | null;
47
+ sellCurrency: string | null;
48
+ }>;
49
+ }
50
+ interface PricingAggregate {
51
+ priceFromAmountCents: number | null;
52
+ priceFromCurrency: string | null;
53
+ hasPricing: boolean;
54
+ }
55
+ /**
56
+ * Pure aggregation kernel — given the product-row pricing and the rule
57
+ * candidate set, produce the projection fields. Exposed via `__test__`
58
+ * for unit coverage that doesn't need a real DB.
59
+ *
60
+ * `productPrice` is the row's `sellAmountCents` (`null` when unset).
61
+ * `currency` is the row's `sellCurrency` (used as the projected
62
+ * currency; rule prices fed in here are pre-filtered to match).
63
+ * `ruleCandidates` is every active default rule's `baseSellAmountCents`
64
+ * AND every active default rule's per-unit `sellAmountCents`, with
65
+ * nulls already filtered out.
66
+ */
67
+ declare function aggregatePricing(productPrice: number | null, currency: string | null, ruleCandidates: ReadonlyArray<number>): PricingAggregate;
68
+ /**
69
+ * Construct the pricing projection extension.
70
+ *
71
+ * Pass `loadProductPricing` in tests to stub the products-row fetch;
72
+ * production uses the default raw-SQL implementation.
73
+ */
74
+ export declare function createProductPricingProjectionExtension(options?: PricingProjectionOptions): ProductProjectionExtension;
75
+ export declare const __test__: {
76
+ aggregatePricing: typeof aggregatePricing;
77
+ EMPTY_AGGREGATE: PricingAggregate;
78
+ };
79
+ export {};
80
+ //# sourceMappingURL=service-catalog-plane-pricing.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"service-catalog-plane-pricing.d.ts","sourceRoot":"","sources":["../src/service-catalog-plane-pricing.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,cAAc,CAAA;AAChD,OAAO,KAAK,EAEV,0BAA0B,EAC3B,MAAM,0CAA0C,CAAA;AAMjD,UAAU,wBAAwB;IAChC;;;;;;;;OAQG;IACH,kBAAkB,CAAC,EAAE,CACnB,EAAE,EAAE,YAAY,EAChB,SAAS,EAAE,MAAM,KACd,OAAO,CAAC;QAAE,eAAe,EAAE,MAAM,GAAG,IAAI,CAAC;QAAC,YAAY,EAAE,MAAM,GAAG,IAAI,CAAA;KAAE,CAAC,CAAA;CAC9E;AAED,UAAU,gBAAgB;IACxB,oBAAoB,EAAE,MAAM,GAAG,IAAI,CAAA;IACnC,iBAAiB,EAAE,MAAM,GAAG,IAAI,CAAA;IAChC,UAAU,EAAE,OAAO,CAAA;CACpB;AAQD;;;;;;;;;;;GAWG;AACH,iBAAS,gBAAgB,CACvB,YAAY,EAAE,MAAM,GAAG,IAAI,EAC3B,QAAQ,EAAE,MAAM,GAAG,IAAI,EACvB,cAAc,EAAE,aAAa,CAAC,MAAM,CAAC,GACpC,gBAAgB,CAqBlB;AAED;;;;;GAKG;AACH,wBAAgB,uCAAuC,CACrD,OAAO,GAAE,wBAA6B,GACrC,0BAA0B,CA4B5B;AA6GD,eAAO,MAAM,QAAQ;;;CAGpB,CAAA"}
@@ -0,0 +1,181 @@
1
+ /**
2
+ * Projection extension that aggregates a "price from" amount across the
3
+ * product's configured default-rule prices and contributes
4
+ * `priceFromAmountCents`, `priceFromCurrency`, and `hasPricing` to the
5
+ * product search document.
6
+ *
7
+ * Lives in `@voyantjs/pricing` because:
8
+ * - The data lives here (`option_price_rules`, `option_unit_price_rules`,
9
+ * `price_catalogs`).
10
+ * - `pricing` already depends on `products`; importing the
11
+ * `ProductProjectionExtension` contract type from products is the
12
+ * same direction. The reverse would create a circular dep.
13
+ *
14
+ * Wire via `createProductDocumentBuilder({ extensions: [pricingExtension] })`
15
+ * after composing `productPricingCatalogPolicy` into the registry.
16
+ *
17
+ * Scope intentionally narrow:
18
+ * - **No schedule-aware rule resolution.** Only `is_default = true`
19
+ * rules contribute. Seasonal / promo rules with schedules don't
20
+ * surface here — they'd require per-slice resolution against a
21
+ * moving "now" date and are deferred to a follow-up.
22
+ * - **No per-departure overrides.** Same reason.
23
+ * - **Currency consistency.** Only rules whose catalog currency matches
24
+ * the product's `sellCurrency` (or whose catalog currency is null
25
+ * and therefore inherits the product's) are MIN'd together. This
26
+ * prevents emitting a misleading "from $50" when one of the rules
27
+ * is actually €50.
28
+ *
29
+ * Document churn: this projection is `now()`-independent — it reads
30
+ * static configured prices, not date-dependent rule windows. Same
31
+ * product reindexed an hour later produces the same fields.
32
+ */
33
+ import { and, eq, isNull, or, sql } from "drizzle-orm";
34
+ import { priceCatalogs } from "./schema-catalogs.js";
35
+ import { optionPriceRules, optionUnitPriceRules } from "./schema-option-rules.js";
36
+ const EMPTY_AGGREGATE = {
37
+ priceFromAmountCents: null,
38
+ priceFromCurrency: null,
39
+ hasPricing: false,
40
+ };
41
+ /**
42
+ * Pure aggregation kernel — given the product-row pricing and the rule
43
+ * candidate set, produce the projection fields. Exposed via `__test__`
44
+ * for unit coverage that doesn't need a real DB.
45
+ *
46
+ * `productPrice` is the row's `sellAmountCents` (`null` when unset).
47
+ * `currency` is the row's `sellCurrency` (used as the projected
48
+ * currency; rule prices fed in here are pre-filtered to match).
49
+ * `ruleCandidates` is every active default rule's `baseSellAmountCents`
50
+ * AND every active default rule's per-unit `sellAmountCents`, with
51
+ * nulls already filtered out.
52
+ */
53
+ function aggregatePricing(productPrice, currency, ruleCandidates) {
54
+ const candidates = [];
55
+ if (productPrice !== null)
56
+ candidates.push(productPrice);
57
+ for (const c of ruleCandidates)
58
+ candidates.push(c);
59
+ if (candidates.length === 0) {
60
+ return { ...EMPTY_AGGREGATE, priceFromCurrency: currency };
61
+ }
62
+ // MIN with a sentinel; faster than spread+Math.min for large arrays
63
+ // and avoids the Math.min stack-arg limit edge case.
64
+ let min = candidates[0];
65
+ for (const c of candidates) {
66
+ if (c < min)
67
+ min = c;
68
+ }
69
+ return {
70
+ priceFromAmountCents: min,
71
+ priceFromCurrency: currency,
72
+ hasPricing: true,
73
+ };
74
+ }
75
+ /**
76
+ * Construct the pricing projection extension.
77
+ *
78
+ * Pass `loadProductPricing` in tests to stub the products-row fetch;
79
+ * production uses the default raw-SQL implementation.
80
+ */
81
+ export function createProductPricingProjectionExtension(options = {}) {
82
+ const loadProductPricing = options.loadProductPricing ?? defaultLoadProductPricing;
83
+ return {
84
+ name: "products:pricing",
85
+ async project(db, productId, _slice) {
86
+ const product = await loadProductPricing(db, productId);
87
+ const currency = product.sellCurrency;
88
+ // Without a product row currency we can't safely filter rules by
89
+ // matching currency — emit only the (null) row pricing. This case
90
+ // is rare in practice (every product has sellCurrency set in the
91
+ // operator UI) but keeps the projection failure-isolated.
92
+ if (!currency) {
93
+ const out = aggregatePricing(product.sellAmountCents, null, []);
94
+ return toProjectionMap(out);
95
+ }
96
+ const [flatPrices, unitPrices] = await Promise.all([
97
+ fetchFlatRulePrices(db, productId, currency),
98
+ fetchUnitRulePrices(db, productId, currency),
99
+ ]);
100
+ const candidates = [...flatPrices, ...unitPrices];
101
+ const out = aggregatePricing(product.sellAmountCents, currency, candidates);
102
+ return toProjectionMap(out);
103
+ },
104
+ };
105
+ }
106
+ /**
107
+ * Read `optionPriceRules.baseSellAmountCents` for the product's active
108
+ * default rules whose catalog currency matches the product currency
109
+ * (or whose catalog has a NULL currency, meaning "inherit product").
110
+ *
111
+ * Filtering on the `(productId, active=true, isDefault=true)` subset
112
+ * keeps the candidate set small even for products with hundreds of
113
+ * seasonal rules.
114
+ */
115
+ async function fetchFlatRulePrices(db, productId, productCurrency) {
116
+ const rows = await db
117
+ .select({ price: optionPriceRules.baseSellAmountCents })
118
+ .from(optionPriceRules)
119
+ .innerJoin(priceCatalogs, eq(priceCatalogs.id, optionPriceRules.priceCatalogId))
120
+ .where(and(eq(optionPriceRules.productId, productId), eq(optionPriceRules.active, true), eq(optionPriceRules.isDefault, true), eq(priceCatalogs.active, true), or(eq(priceCatalogs.currencyCode, productCurrency), isNull(priceCatalogs.currencyCode))));
121
+ const out = [];
122
+ for (const row of rows) {
123
+ if (row.price !== null)
124
+ out.push(row.price);
125
+ }
126
+ return out;
127
+ }
128
+ /**
129
+ * Read `optionUnitPriceRules.sellAmountCents` for active per-unit tiers
130
+ * whose parent rule is one of the product's active default rules in a
131
+ * matching-currency catalog. Used for products priced per occupancy
132
+ * (e.g. cabins at "single $X / double $Y / triple $Z") where the parent
133
+ * rule's `baseSellAmountCents` is null and the actual prices live on
134
+ * the unit tiers.
135
+ */
136
+ async function fetchUnitRulePrices(db, productId, productCurrency) {
137
+ const rows = await db
138
+ .select({ price: optionUnitPriceRules.sellAmountCents })
139
+ .from(optionUnitPriceRules)
140
+ .innerJoin(optionPriceRules, eq(optionPriceRules.id, optionUnitPriceRules.optionPriceRuleId))
141
+ .innerJoin(priceCatalogs, eq(priceCatalogs.id, optionPriceRules.priceCatalogId))
142
+ .where(and(eq(optionPriceRules.productId, productId), eq(optionPriceRules.active, true), eq(optionPriceRules.isDefault, true), eq(optionUnitPriceRules.active, true), eq(priceCatalogs.active, true), or(eq(priceCatalogs.currencyCode, productCurrency), isNull(priceCatalogs.currencyCode))));
143
+ const out = [];
144
+ for (const row of rows) {
145
+ if (row.price !== null)
146
+ out.push(row.price);
147
+ }
148
+ return out;
149
+ }
150
+ /**
151
+ * Default loader reads the products row via raw SQL so we don't pull
152
+ * the products schema into this file (would create a circular import
153
+ * via the typed schema). The columns we read are stable enough that a
154
+ * rename would break far more than this.
155
+ */
156
+ async function defaultLoadProductPricing(db, productId) {
157
+ // biome-ignore lint/suspicious/noExplicitAny: drizzle's typed sql is overkill for two-column read
158
+ const dbAny = db;
159
+ const result = await dbAny.execute(sql `SELECT sell_amount_cents, sell_currency FROM products WHERE id = ${productId} LIMIT 1`);
160
+ // postgres-js returns rows as an array-like; node-postgres returns `{ rows: [...] }`.
161
+ const rows = Array.isArray(result) ? result : (result?.rows ?? []);
162
+ const first = rows[0];
163
+ if (!first)
164
+ return { sellAmountCents: null, sellCurrency: null };
165
+ return {
166
+ sellAmountCents: first.sell_amount_cents,
167
+ sellCurrency: first.sell_currency,
168
+ };
169
+ }
170
+ function toProjectionMap(a) {
171
+ return new Map([
172
+ ["priceFromAmountCents", a.priceFromAmountCents],
173
+ ["priceFromCurrency", a.priceFromCurrency],
174
+ ["hasPricing", a.hasPricing],
175
+ ]);
176
+ }
177
+ // Internal exports for unit tests — kept off the public surface.
178
+ export const __test__ = {
179
+ aggregatePricing,
180
+ EMPTY_AGGREGATE,
181
+ };
@@ -33,8 +33,8 @@ export declare function getPriceCatalogById(db: PostgresJsDatabase, id: string):
33
33
  } | null>;
34
34
  export declare function createPriceCatalog(db: PostgresJsDatabase, data: CreatePriceCatalogInput): Promise<{
35
35
  metadata: Record<string, unknown> | null;
36
- id: string;
37
36
  name: string;
37
+ id: string;
38
38
  createdAt: Date;
39
39
  updatedAt: Date;
40
40
  notes: string | null;
@@ -101,8 +101,8 @@ export declare function getPriceScheduleById(db: PostgresJsDatabase, id: string)
101
101
  } | null>;
102
102
  export declare function createPriceSchedule(db: PostgresJsDatabase, data: CreatePriceScheduleInput): Promise<{
103
103
  metadata: Record<string, unknown> | null;
104
- id: string;
105
104
  name: string;
105
+ id: string;
106
106
  createdAt: Date;
107
107
  updatedAt: Date;
108
108
  notes: string | null;
@@ -8,7 +8,7 @@ export declare function listPricingCategories(db: PostgresJsDatabase, query: Pri
8
8
  unitId: string | null;
9
9
  code: string | null;
10
10
  name: string;
11
- categoryType: "service" | "other" | "adult" | "child" | "infant" | "senior" | "group" | "room" | "vehicle";
11
+ categoryType: "service" | "child" | "other" | "adult" | "infant" | "senior" | "group" | "room" | "vehicle";
12
12
  seatOccupancy: number;
13
13
  groupSize: number | null;
14
14
  isAgeQualified: boolean;
@@ -32,7 +32,7 @@ export declare function getPricingCategoryById(db: PostgresJsDatabase, id: strin
32
32
  unitId: string | null;
33
33
  code: string | null;
34
34
  name: string;
35
- categoryType: "service" | "other" | "adult" | "child" | "infant" | "senior" | "group" | "room" | "vehicle";
35
+ categoryType: "service" | "child" | "other" | "adult" | "infant" | "senior" | "group" | "room" | "vehicle";
36
36
  seatOccupancy: number;
37
37
  groupSize: number | null;
38
38
  isAgeQualified: boolean;
@@ -47,8 +47,8 @@ export declare function getPricingCategoryById(db: PostgresJsDatabase, id: strin
47
47
  } | null>;
48
48
  export declare function createPricingCategory(db: PostgresJsDatabase, data: CreatePricingCategoryInput): Promise<{
49
49
  metadata: Record<string, unknown> | null;
50
- id: string;
51
50
  name: string;
51
+ id: string;
52
52
  createdAt: Date;
53
53
  updatedAt: Date;
54
54
  code: string | null;
@@ -56,7 +56,7 @@ export declare function createPricingCategory(db: PostgresJsDatabase, data: Crea
56
56
  productId: string | null;
57
57
  optionId: string | null;
58
58
  unitId: string | null;
59
- categoryType: "service" | "other" | "adult" | "child" | "infant" | "senior" | "group" | "room" | "vehicle";
59
+ categoryType: "service" | "child" | "other" | "adult" | "infant" | "senior" | "group" | "room" | "vehicle";
60
60
  seatOccupancy: number;
61
61
  groupSize: number | null;
62
62
  isAgeQualified: boolean;
@@ -72,7 +72,7 @@ export declare function updatePricingCategory(db: PostgresJsDatabase, id: string
72
72
  unitId: string | null;
73
73
  code: string | null;
74
74
  name: string;
75
- categoryType: "service" | "other" | "adult" | "child" | "infant" | "senior" | "group" | "room" | "vehicle";
75
+ categoryType: "service" | "child" | "other" | "adult" | "infant" | "senior" | "group" | "room" | "vehicle";
76
76
  seatOccupancy: number;
77
77
  groupSize: number | null;
78
78
  isAgeQualified: boolean;
@@ -1,4 +1,18 @@
1
+ import type { EventBus } from "@voyantjs/core";
1
2
  import type { PostgresJsDatabase } from "drizzle-orm/postgres-js";
3
+ import { type PricingRuleChangeSource } from "./events.js";
4
+ /**
5
+ * Optional runtime context for pricing-rule mutations. When `eventBus`
6
+ * is wired the service emits `pricing.rule.changed` after a successful
7
+ * mutation so the catalog bridge can reindex the affected product.
8
+ *
9
+ * Keeping it optional preserves back-compat with existing callers that
10
+ * don't care about the catalog plane (e.g. data-import scripts).
11
+ */
12
+ export interface RuleMutationRuntime {
13
+ eventBus?: EventBus;
14
+ source?: PricingRuleChangeSource;
15
+ }
2
16
  import type { CreateOptionPriceRuleInput, CreateOptionStartTimeRuleInput, CreateOptionUnitPriceRuleInput, CreateOptionUnitTierInput, OptionPriceRuleListQuery, OptionStartTimeRuleListQuery, OptionUnitPriceRuleListQuery, OptionUnitTierListQuery, UpdateOptionPriceRuleInput, UpdateOptionStartTimeRuleInput, UpdateOptionUnitPriceRuleInput, UpdateOptionUnitTierInput } from "./service-shared.js";
3
17
  export declare function listOptionPriceRules(db: PostgresJsDatabase, query: OptionPriceRuleListQuery): Promise<{
4
18
  data: {
@@ -51,10 +65,10 @@ export declare function getOptionPriceRuleById(db: PostgresJsDatabase, id: strin
51
65
  createdAt: Date;
52
66
  updatedAt: Date;
53
67
  } | null>;
54
- export declare function createOptionPriceRule(db: PostgresJsDatabase, data: CreateOptionPriceRuleInput): Promise<{
68
+ export declare function createOptionPriceRule(db: PostgresJsDatabase, data: CreateOptionPriceRuleInput, runtime?: RuleMutationRuntime): Promise<{
55
69
  metadata: Record<string, unknown> | null;
56
- id: string;
57
70
  name: string;
71
+ id: string;
58
72
  createdAt: Date;
59
73
  updatedAt: Date;
60
74
  notes: string | null;
@@ -74,7 +88,7 @@ export declare function createOptionPriceRule(db: PostgresJsDatabase, data: Crea
74
88
  maxPerBooking: number | null;
75
89
  allPricingCategories: boolean;
76
90
  } | null>;
77
- export declare function updateOptionPriceRule(db: PostgresJsDatabase, id: string, data: UpdateOptionPriceRuleInput): Promise<{
91
+ export declare function updateOptionPriceRule(db: PostgresJsDatabase, id: string, data: UpdateOptionPriceRuleInput, runtime?: RuleMutationRuntime): Promise<{
78
92
  id: string;
79
93
  productId: string;
80
94
  optionId: string;
@@ -97,7 +111,7 @@ export declare function updateOptionPriceRule(db: PostgresJsDatabase, id: string
97
111
  createdAt: Date;
98
112
  updatedAt: Date;
99
113
  } | null>;
100
- export declare function deleteOptionPriceRule(db: PostgresJsDatabase, id: string): Promise<{
114
+ export declare function deleteOptionPriceRule(db: PostgresJsDatabase, id: string, runtime?: RuleMutationRuntime): Promise<{
101
115
  id: string;
102
116
  } | null>;
103
117
  export declare function listOptionUnitPriceRules(db: PostgresJsDatabase, query: OptionUnitPriceRuleListQuery): Promise<{
@@ -141,7 +155,7 @@ export declare function getOptionUnitPriceRuleById(db: PostgresJsDatabase, id: s
141
155
  createdAt: Date;
142
156
  updatedAt: Date;
143
157
  } | null>;
144
- export declare function createOptionUnitPriceRule(db: PostgresJsDatabase, data: CreateOptionUnitPriceRuleInput): Promise<{
158
+ export declare function createOptionUnitPriceRule(db: PostgresJsDatabase, data: CreateOptionUnitPriceRuleInput, runtime?: RuleMutationRuntime): Promise<{
145
159
  metadata: Record<string, unknown> | null;
146
160
  id: string;
147
161
  createdAt: Date;
@@ -159,7 +173,7 @@ export declare function createOptionUnitPriceRule(db: PostgresJsDatabase, data:
159
173
  minQuantity: number | null;
160
174
  maxQuantity: number | null;
161
175
  } | null>;
162
- export declare function updateOptionUnitPriceRule(db: PostgresJsDatabase, id: string, data: UpdateOptionUnitPriceRuleInput): Promise<{
176
+ export declare function updateOptionUnitPriceRule(db: PostgresJsDatabase, id: string, data: UpdateOptionUnitPriceRuleInput, runtime?: RuleMutationRuntime): Promise<{
163
177
  id: string;
164
178
  optionPriceRuleId: string;
165
179
  optionId: string;
@@ -177,7 +191,7 @@ export declare function updateOptionUnitPriceRule(db: PostgresJsDatabase, id: st
177
191
  createdAt: Date;
178
192
  updatedAt: Date;
179
193
  } | null>;
180
- export declare function deleteOptionUnitPriceRule(db: PostgresJsDatabase, id: string): Promise<{
194
+ export declare function deleteOptionUnitPriceRule(db: PostgresJsDatabase, id: string, runtime?: RuleMutationRuntime): Promise<{
181
195
  id: string;
182
196
  } | null>;
183
197
  export declare function listOptionStartTimeRules(db: PostgresJsDatabase, query: OptionStartTimeRuleListQuery): Promise<{
@@ -1 +1 @@
1
- {"version":3,"file":"service-option-rules.d.ts","sourceRoot":"","sources":["../src/service-option-rules.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,yBAAyB,CAAA;AAQjE,OAAO,KAAK,EACV,0BAA0B,EAC1B,8BAA8B,EAC9B,8BAA8B,EAC9B,yBAAyB,EACzB,wBAAwB,EACxB,4BAA4B,EAC5B,4BAA4B,EAC5B,uBAAuB,EACvB,0BAA0B,EAC1B,8BAA8B,EAC9B,8BAA8B,EAC9B,yBAAyB,EAC1B,MAAM,qBAAqB,CAAA;AAG5B,wBAAsB,oBAAoB,CACxC,EAAE,EAAE,kBAAkB,EACtB,KAAK,EAAE,wBAAwB;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BhC;AAED,wBAAsB,sBAAsB,CAAC,EAAE,EAAE,kBAAkB,EAAE,EAAE,EAAE,MAAM;;;;;;;;;;;;;;;;;;;;;;UAG9E;AAED,wBAAsB,qBAAqB,CACzC,EAAE,EAAE,kBAAkB,EACtB,IAAI,EAAE,0BAA0B;;;;;;;;;;;;;;;;;;;;;;UAIjC;AAED,wBAAsB,qBAAqB,CACzC,EAAE,EAAE,kBAAkB,EACtB,EAAE,EAAE,MAAM,EACV,IAAI,EAAE,0BAA0B;;;;;;;;;;;;;;;;;;;;;;UAQjC;AAED,wBAAsB,qBAAqB,CAAC,EAAE,EAAE,kBAAkB,EAAE,EAAE,EAAE,MAAM;;UAM7E;AAED,wBAAsB,wBAAwB,CAC5C,EAAE,EAAE,kBAAkB,EACtB,KAAK,EAAE,4BAA4B;;;;;;;;;;;;;;;;;;;;;;GA0BpC;AAED,wBAAsB,0BAA0B,CAAC,EAAE,EAAE,kBAAkB,EAAE,EAAE,EAAE,MAAM;;;;;;;;;;;;;;;;;UAOlF;AAED,wBAAsB,yBAAyB,CAC7C,EAAE,EAAE,kBAAkB,EACtB,IAAI,EAAE,8BAA8B;;;;;;;;;;;;;;;;;UAIrC;AAED,wBAAsB,yBAAyB,CAC7C,EAAE,EAAE,kBAAkB,EACtB,EAAE,EAAE,MAAM,EACV,IAAI,EAAE,8BAA8B;;;;;;;;;;;;;;;;;UAQrC;AAED,wBAAsB,yBAAyB,CAAC,EAAE,EAAE,kBAAkB,EAAE,EAAE,EAAE,MAAM;;UAMjF;AAED,wBAAsB,wBAAwB,CAC5C,EAAE,EAAE,kBAAkB,EACtB,KAAK,EAAE,4BAA4B;;;;;;;;;;;;;;;;;;;GAuBpC;AAED,wBAAsB,0BAA0B,CAAC,EAAE,EAAE,kBAAkB,EAAE,EAAE,EAAE,MAAM;;;;;;;;;;;;;;UAOlF;AAED,wBAAsB,yBAAyB,CAC7C,EAAE,EAAE,kBAAkB,EACtB,IAAI,EAAE,8BAA8B;;;;;;;;;;;;;;UAIrC;AAED,wBAAsB,yBAAyB,CAC7C,EAAE,EAAE,kBAAkB,EACtB,EAAE,EAAE,MAAM,EACV,IAAI,EAAE,8BAA8B;;;;;;;;;;;;;;UAQrC;AAED,wBAAsB,yBAAyB,CAAC,EAAE,EAAE,kBAAkB,EAAE,EAAE,EAAE,MAAM;;UAMjF;AAED,wBAAsB,mBAAmB,CAAC,EAAE,EAAE,kBAAkB,EAAE,KAAK,EAAE,uBAAuB;;;;;;;;;;;;;;;;GAoB/F;AAED,wBAAsB,qBAAqB,CAAC,EAAE,EAAE,kBAAkB,EAAE,EAAE,EAAE,MAAM;;;;;;;;;;;UAG7E;AAED,wBAAsB,oBAAoB,CACxC,EAAE,EAAE,kBAAkB,EACtB,IAAI,EAAE,yBAAyB;;;;;;;;;;;UAIhC;AAED,wBAAsB,oBAAoB,CACxC,EAAE,EAAE,kBAAkB,EACtB,EAAE,EAAE,MAAM,EACV,IAAI,EAAE,yBAAyB;;;;;;;;;;;UAQhC;AAED,wBAAsB,oBAAoB,CAAC,EAAE,EAAE,kBAAkB,EAAE,EAAE,EAAE,MAAM;;UAM5E"}
1
+ {"version":3,"file":"service-option-rules.d.ts","sourceRoot":"","sources":["../src/service-option-rules.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAA;AAG9C,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,yBAAyB,CAAA;AAEjE,OAAO,EAGL,KAAK,uBAAuB,EAC7B,MAAM,aAAa,CAAA;AAQpB;;;;;;;GAOG;AACH,MAAM,WAAW,mBAAmB;IAClC,QAAQ,CAAC,EAAE,QAAQ,CAAA;IACnB,MAAM,CAAC,EAAE,uBAAuB,CAAA;CACjC;AAqBD,OAAO,KAAK,EACV,0BAA0B,EAC1B,8BAA8B,EAC9B,8BAA8B,EAC9B,yBAAyB,EACzB,wBAAwB,EACxB,4BAA4B,EAC5B,4BAA4B,EAC5B,uBAAuB,EACvB,0BAA0B,EAC1B,8BAA8B,EAC9B,8BAA8B,EAC9B,yBAAyB,EAC1B,MAAM,qBAAqB,CAAA;AAG5B,wBAAsB,oBAAoB,CACxC,EAAE,EAAE,kBAAkB,EACtB,KAAK,EAAE,wBAAwB;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BhC;AAED,wBAAsB,sBAAsB,CAAC,EAAE,EAAE,kBAAkB,EAAE,EAAE,EAAE,MAAM;;;;;;;;;;;;;;;;;;;;;;UAG9E;AAED,wBAAsB,qBAAqB,CACzC,EAAE,EAAE,kBAAkB,EACtB,IAAI,EAAE,0BAA0B,EAChC,OAAO,GAAE,mBAAwB;;;;;;;;;;;;;;;;;;;;;;UAWlC;AAED,wBAAsB,qBAAqB,CACzC,EAAE,EAAE,kBAAkB,EACtB,EAAE,EAAE,MAAM,EACV,IAAI,EAAE,0BAA0B,EAChC,OAAO,GAAE,mBAAwB;;;;;;;;;;;;;;;;;;;;;;UAgDlC;AAED,wBAAsB,qBAAqB,CACzC,EAAE,EAAE,kBAAkB,EACtB,EAAE,EAAE,MAAM,EACV,OAAO,GAAE,mBAAwB;;UAyBlC;AAED,wBAAsB,wBAAwB,CAC5C,EAAE,EAAE,kBAAkB,EACtB,KAAK,EAAE,4BAA4B;;;;;;;;;;;;;;;;;;;;;;GA0BpC;AAED,wBAAsB,0BAA0B,CAAC,EAAE,EAAE,kBAAkB,EAAE,EAAE,EAAE,MAAM;;;;;;;;;;;;;;;;;UAOlF;AAsBD,wBAAsB,yBAAyB,CAC7C,EAAE,EAAE,kBAAkB,EACtB,IAAI,EAAE,8BAA8B,EACpC,OAAO,GAAE,mBAAwB;;;;;;;;;;;;;;;;;UAwBlC;AAED,wBAAsB,yBAAyB,CAC7C,EAAE,EAAE,kBAAkB,EACtB,EAAE,EAAE,MAAM,EACV,IAAI,EAAE,8BAA8B,EACpC,OAAO,GAAE,mBAAwB;;;;;;;;;;;;;;;;;UAkClC;AAED,wBAAsB,yBAAyB,CAC7C,EAAE,EAAE,kBAAkB,EACtB,EAAE,EAAE,MAAM,EACV,OAAO,GAAE,mBAAwB;;UAoBlC;AAED,wBAAsB,wBAAwB,CAC5C,EAAE,EAAE,kBAAkB,EACtB,KAAK,EAAE,4BAA4B;;;;;;;;;;;;;;;;;;;GAuBpC;AAED,wBAAsB,0BAA0B,CAAC,EAAE,EAAE,kBAAkB,EAAE,EAAE,EAAE,MAAM;;;;;;;;;;;;;;UAOlF;AAED,wBAAsB,yBAAyB,CAC7C,EAAE,EAAE,kBAAkB,EACtB,IAAI,EAAE,8BAA8B;;;;;;;;;;;;;;UAIrC;AAED,wBAAsB,yBAAyB,CAC7C,EAAE,EAAE,kBAAkB,EACtB,EAAE,EAAE,MAAM,EACV,IAAI,EAAE,8BAA8B;;;;;;;;;;;;;;UAQrC;AAED,wBAAsB,yBAAyB,CAAC,EAAE,EAAE,kBAAkB,EAAE,EAAE,EAAE,MAAM;;UAMjF;AAED,wBAAsB,mBAAmB,CAAC,EAAE,EAAE,kBAAkB,EAAE,KAAK,EAAE,uBAAuB;;;;;;;;;;;;;;;;GAoB/F;AAED,wBAAsB,qBAAqB,CAAC,EAAE,EAAE,kBAAkB,EAAE,EAAE,EAAE,MAAM;;;;;;;;;;;UAG7E;AAED,wBAAsB,oBAAoB,CACxC,EAAE,EAAE,kBAAkB,EACtB,IAAI,EAAE,yBAAyB;;;;;;;;;;;UAIhC;AAED,wBAAsB,oBAAoB,CACxC,EAAE,EAAE,kBAAkB,EACtB,EAAE,EAAE,MAAM,EACV,IAAI,EAAE,yBAAyB;;;;;;;;;;;UAQhC;AAED,wBAAsB,oBAAoB,CAAC,EAAE,EAAE,kBAAkB,EAAE,EAAE,EAAE,MAAM;;UAM5E"}
@@ -1,5 +1,21 @@
1
+ import { RequestValidationError } from "@voyantjs/hono";
1
2
  import { and, asc, desc, eq, sql } from "drizzle-orm";
3
+ import { PRICING_RULE_CHANGED_EVENT, } from "./events.js";
2
4
  import { optionPriceRules, optionStartTimeRules, optionUnitPriceRules, optionUnitTiers, } from "./schema.js";
5
+ async function emitRuleChanged(eventBus, payload) {
6
+ if (!eventBus)
7
+ return;
8
+ await eventBus.emit(PRICING_RULE_CHANGED_EVENT, payload, {
9
+ category: "domain",
10
+ source: "service",
11
+ });
12
+ }
13
+ // A `per_booking` rule produces a single flat amount for the whole booking;
14
+ // per-unit prices implicitly assume a per-unit (or per-person) multiplier.
15
+ // The two are contradictory — see #482.
16
+ const PER_BOOKING_REJECTS_UNIT_PRICES_MESSAGE = "Rules with pricingMode = 'per_booking' cannot carry per-unit prices. " +
17
+ "Use pricingMode = 'per_person' or 'starting_from' for unit-priced rules, " +
18
+ "or remove the unit prices to keep this rule a flat per-booking amount.";
3
19
  import { paginate } from "./service-shared.js";
4
20
  export async function listOptionPriceRules(db, query) {
5
21
  const conditions = [];
@@ -31,24 +47,87 @@ export async function getOptionPriceRuleById(db, id) {
31
47
  const [row] = await db.select().from(optionPriceRules).where(eq(optionPriceRules.id, id)).limit(1);
32
48
  return row ?? null;
33
49
  }
34
- export async function createOptionPriceRule(db, data) {
50
+ export async function createOptionPriceRule(db, data, runtime = {}) {
35
51
  const [row] = await db.insert(optionPriceRules).values(data).returning();
36
- return row ?? null;
52
+ if (!row)
53
+ return null;
54
+ await emitRuleChanged(runtime.eventBus, {
55
+ productId: row.productId,
56
+ ruleId: row.id,
57
+ kind: "option-rule",
58
+ source: runtime.source ?? "created",
59
+ });
60
+ return row;
37
61
  }
38
- export async function updateOptionPriceRule(db, id, data) {
62
+ export async function updateOptionPriceRule(db, id, data, runtime = {}) {
63
+ if (data.pricingMode === "per_booking") {
64
+ const [countRow] = await db
65
+ .select({ count: sql `count(*)::int` })
66
+ .from(optionUnitPriceRules)
67
+ .where(eq(optionUnitPriceRules.optionPriceRuleId, id));
68
+ const unitPriceCount = countRow?.count ?? 0;
69
+ if (unitPriceCount > 0) {
70
+ throw new RequestValidationError(PER_BOOKING_REJECTS_UNIT_PRICES_MESSAGE, {
71
+ ruleId: id,
72
+ unitPriceCount,
73
+ });
74
+ }
75
+ }
76
+ // Snapshot the pre-update productId so reassignment (rule moves
77
+ // between products) reindexes the *previous* product too. Without
78
+ // this, the projection on the old product keeps a stale MIN that
79
+ // includes a rule it no longer owns.
80
+ const [pre] = await db
81
+ .select({ productId: optionPriceRules.productId })
82
+ .from(optionPriceRules)
83
+ .where(eq(optionPriceRules.id, id))
84
+ .limit(1);
39
85
  const [row] = await db
40
86
  .update(optionPriceRules)
41
87
  .set({ ...data, updatedAt: new Date() })
42
88
  .where(eq(optionPriceRules.id, id))
43
89
  .returning();
44
- return row ?? null;
90
+ if (!row)
91
+ return null;
92
+ await emitRuleChanged(runtime.eventBus, {
93
+ productId: row.productId,
94
+ ruleId: row.id,
95
+ kind: "option-rule",
96
+ source: runtime.source ?? "updated",
97
+ });
98
+ if (pre && pre.productId !== row.productId) {
99
+ await emitRuleChanged(runtime.eventBus, {
100
+ productId: pre.productId,
101
+ ruleId: row.id,
102
+ kind: "option-rule",
103
+ source: runtime.source ?? "updated",
104
+ });
105
+ }
106
+ return row;
45
107
  }
46
- export async function deleteOptionPriceRule(db, id) {
108
+ export async function deleteOptionPriceRule(db, id, runtime = {}) {
109
+ // Snapshot before deletion so the event payload carries productId —
110
+ // can't read it back from the deleted row.
111
+ const [snapshot] = await db
112
+ .select({ productId: optionPriceRules.productId })
113
+ .from(optionPriceRules)
114
+ .where(eq(optionPriceRules.id, id))
115
+ .limit(1);
47
116
  const [row] = await db
48
117
  .delete(optionPriceRules)
49
118
  .where(eq(optionPriceRules.id, id))
50
119
  .returning({ id: optionPriceRules.id });
51
- return row ?? null;
120
+ if (!row)
121
+ return null;
122
+ if (snapshot) {
123
+ await emitRuleChanged(runtime.eventBus, {
124
+ productId: snapshot.productId,
125
+ ruleId: row.id,
126
+ kind: "option-rule",
127
+ source: runtime.source ?? "deleted",
128
+ });
129
+ }
130
+ return row;
52
131
  }
53
132
  export async function listOptionUnitPriceRules(db, query) {
54
133
  const conditions = [];
@@ -81,24 +160,97 @@ export async function getOptionUnitPriceRuleById(db, id) {
81
160
  .limit(1);
82
161
  return row ?? null;
83
162
  }
84
- export async function createOptionUnitPriceRule(db, data) {
163
+ /**
164
+ * Look up the productId on an option-unit-rule's parent rule. Used by
165
+ * the mutation paths to populate the `pricing.rule.changed` payload —
166
+ * unit rules don't carry productId directly, so we walk through their
167
+ * parent every time. One small extra query per mutation; pricing
168
+ * mutations aren't on a hot path so the cost is negligible.
169
+ */
170
+ async function getProductIdForUnitRule(db, unitRuleId) {
171
+ const [row] = await db
172
+ .select({ productId: optionPriceRules.productId })
173
+ .from(optionUnitPriceRules)
174
+ .innerJoin(optionPriceRules, eq(optionPriceRules.id, optionUnitPriceRules.optionPriceRuleId))
175
+ .where(eq(optionUnitPriceRules.id, unitRuleId))
176
+ .limit(1);
177
+ return row?.productId ?? null;
178
+ }
179
+ export async function createOptionUnitPriceRule(db, data, runtime = {}) {
180
+ const [parent] = await db
181
+ .select({ pricingMode: optionPriceRules.pricingMode, productId: optionPriceRules.productId })
182
+ .from(optionPriceRules)
183
+ .where(eq(optionPriceRules.id, data.optionPriceRuleId))
184
+ .limit(1);
185
+ if (parent?.pricingMode === "per_booking") {
186
+ throw new RequestValidationError(PER_BOOKING_REJECTS_UNIT_PRICES_MESSAGE, {
187
+ ruleId: data.optionPriceRuleId,
188
+ });
189
+ }
85
190
  const [row] = await db.insert(optionUnitPriceRules).values(data).returning();
86
- return row ?? null;
191
+ if (!row)
192
+ return null;
193
+ if (parent) {
194
+ await emitRuleChanged(runtime.eventBus, {
195
+ productId: parent.productId,
196
+ ruleId: row.id,
197
+ kind: "option-unit-rule",
198
+ source: runtime.source ?? "created",
199
+ });
200
+ }
201
+ return row;
87
202
  }
88
- export async function updateOptionUnitPriceRule(db, id, data) {
203
+ export async function updateOptionUnitPriceRule(db, id, data, runtime = {}) {
204
+ // Snapshot the pre-update parent's productId. If the update reassigns
205
+ // `optionPriceRuleId` to a parent rule under a different product, the
206
+ // *previous* product also loses this unit-rule from its MIN
207
+ // candidate set and needs reindexing. Without this snapshot the old
208
+ // product's `priceFromAmountCents` stays stale.
209
+ const prevProductId = await getProductIdForUnitRule(db, id);
89
210
  const [row] = await db
90
211
  .update(optionUnitPriceRules)
91
212
  .set({ ...data, updatedAt: new Date() })
92
213
  .where(eq(optionUnitPriceRules.id, id))
93
214
  .returning();
94
- return row ?? null;
215
+ if (!row)
216
+ return null;
217
+ const nextProductId = await getProductIdForUnitRule(db, row.id);
218
+ if (nextProductId) {
219
+ await emitRuleChanged(runtime.eventBus, {
220
+ productId: nextProductId,
221
+ ruleId: row.id,
222
+ kind: "option-unit-rule",
223
+ source: runtime.source ?? "updated",
224
+ });
225
+ }
226
+ if (prevProductId && prevProductId !== nextProductId) {
227
+ await emitRuleChanged(runtime.eventBus, {
228
+ productId: prevProductId,
229
+ ruleId: row.id,
230
+ kind: "option-unit-rule",
231
+ source: runtime.source ?? "updated",
232
+ });
233
+ }
234
+ return row;
95
235
  }
96
- export async function deleteOptionUnitPriceRule(db, id) {
236
+ export async function deleteOptionUnitPriceRule(db, id, runtime = {}) {
237
+ // Snapshot productId before deletion — the row is gone after.
238
+ const productId = await getProductIdForUnitRule(db, id);
97
239
  const [row] = await db
98
240
  .delete(optionUnitPriceRules)
99
241
  .where(eq(optionUnitPriceRules.id, id))
100
242
  .returning({ id: optionUnitPriceRules.id });
101
- return row ?? null;
243
+ if (!row)
244
+ return null;
245
+ if (productId) {
246
+ await emitRuleChanged(runtime.eventBus, {
247
+ productId,
248
+ ruleId: row.id,
249
+ kind: "option-unit-rule",
250
+ source: runtime.source ?? "deleted",
251
+ });
252
+ }
253
+ return row;
102
254
  }
103
255
  export async function listOptionStartTimeRules(db, query) {
104
256
  const conditions = [];
@@ -33,8 +33,8 @@ export declare function getCancellationPolicyById(db: PostgresJsDatabase, id: st
33
33
  } | null>;
34
34
  export declare function createCancellationPolicy(db: PostgresJsDatabase, data: CreateCancellationPolicyInput): Promise<{
35
35
  metadata: Record<string, unknown> | null;
36
- id: string;
37
36
  name: string;
37
+ id: string;
38
38
  createdAt: Date;
39
39
  updatedAt: Date;
40
40
  notes: string | null;
@@ -68,7 +68,7 @@ export declare const publicPricingService: {
68
68
  startsAt: string | null;
69
69
  endsAt: string | null;
70
70
  timezone: string;
71
- status: "open" | "closed" | "sold_out" | "cancelled";
71
+ status: "cancelled" | "open" | "closed" | "sold_out";
72
72
  unlimited: boolean;
73
73
  remainingPax: number | null;
74
74
  remainingResources: number | null;
@@ -10,10 +10,10 @@ export declare const publicAvailabilitySnapshotQuerySchema: z.ZodObject<{
10
10
  dateFrom: z.ZodOptional<z.ZodString>;
11
11
  dateTo: z.ZodOptional<z.ZodString>;
12
12
  status: z.ZodOptional<z.ZodEnum<{
13
+ cancelled: "cancelled";
13
14
  open: "open";
14
15
  closed: "closed";
15
16
  sold_out: "sold_out";
16
- cancelled: "cancelled";
17
17
  }>>;
18
18
  limit: z.ZodDefault<z.ZodCoercedNumber<unknown>>;
19
19
  offset: z.ZodDefault<z.ZodCoercedNumber<unknown>>;
@@ -359,10 +359,10 @@ export declare const publicAvailabilitySlotSchema: z.ZodObject<{
359
359
  endsAt: z.ZodNullable<z.ZodString>;
360
360
  timezone: z.ZodString;
361
361
  status: z.ZodEnum<{
362
+ cancelled: "cancelled";
362
363
  open: "open";
363
364
  closed: "closed";
364
365
  sold_out: "sold_out";
365
- cancelled: "cancelled";
366
366
  }>;
367
367
  unlimited: z.ZodBoolean;
368
368
  remainingPax: z.ZodNullable<z.ZodNumber>;
@@ -386,10 +386,10 @@ export declare const publicAvailabilitySnapshotSchema: z.ZodObject<{
386
386
  endsAt: z.ZodNullable<z.ZodString>;
387
387
  timezone: z.ZodString;
388
388
  status: z.ZodEnum<{
389
+ cancelled: "cancelled";
389
390
  open: "open";
390
391
  closed: "closed";
391
392
  sold_out: "sold_out";
392
- cancelled: "cancelled";
393
393
  }>;
394
394
  unlimited: z.ZodBoolean;
395
395
  remainingPax: z.ZodNullable<z.ZodNumber>;
@@ -1,9 +1,9 @@
1
1
  import { z } from "zod";
2
2
  export declare const pricingCategoryTypeSchema: z.ZodEnum<{
3
3
  service: "service";
4
+ child: "child";
4
5
  other: "other";
5
6
  adult: "adult";
6
- child: "child";
7
7
  infant: "infant";
8
8
  senior: "senior";
9
9
  group: "group";
@@ -7,9 +7,9 @@ export declare const pricingCategoryCoreSchema: z.ZodObject<{
7
7
  name: z.ZodString;
8
8
  categoryType: z.ZodDefault<z.ZodEnum<{
9
9
  service: "service";
10
+ child: "child";
10
11
  other: "other";
11
12
  adult: "adult";
12
- child: "child";
13
13
  infant: "infant";
14
14
  senior: "senior";
15
15
  group: "group";
@@ -34,9 +34,9 @@ export declare const insertPricingCategorySchema: z.ZodObject<{
34
34
  name: z.ZodString;
35
35
  categoryType: z.ZodDefault<z.ZodEnum<{
36
36
  service: "service";
37
+ child: "child";
37
38
  other: "other";
38
39
  adult: "adult";
39
- child: "child";
40
40
  infant: "infant";
41
41
  senior: "senior";
42
42
  group: "group";
@@ -61,9 +61,9 @@ export declare const updatePricingCategorySchema: z.ZodObject<{
61
61
  name: z.ZodOptional<z.ZodString>;
62
62
  categoryType: z.ZodOptional<z.ZodDefault<z.ZodEnum<{
63
63
  service: "service";
64
+ child: "child";
64
65
  other: "other";
65
66
  adult: "adult";
66
- child: "child";
67
67
  infant: "infant";
68
68
  senior: "senior";
69
69
  group: "group";
@@ -88,9 +88,9 @@ export declare const pricingCategoryListQuerySchema: z.ZodObject<{
88
88
  unitId: z.ZodOptional<z.ZodString>;
89
89
  categoryType: z.ZodOptional<z.ZodEnum<{
90
90
  service: "service";
91
+ child: "child";
91
92
  other: "other";
92
93
  adult: "adult";
93
- child: "child";
94
94
  infant: "infant";
95
95
  senior: "senior";
96
96
  group: "group";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@voyantjs/pricing",
3
- "version": "0.28.1",
3
+ "version": "0.29.0",
4
4
  "license": "Apache-2.0",
5
5
  "type": "module",
6
6
  "exports": {
@@ -33,6 +33,11 @@
33
33
  "types": "./dist/validation-public.d.ts",
34
34
  "import": "./dist/validation-public.js",
35
35
  "default": "./dist/validation-public.js"
36
+ },
37
+ "./service-catalog-plane-pricing": {
38
+ "types": "./dist/service-catalog-plane-pricing.d.ts",
39
+ "import": "./dist/service-catalog-plane-pricing.js",
40
+ "default": "./dist/service-catalog-plane-pricing.js"
36
41
  }
37
42
  },
38
43
  "dependencies": {
@@ -40,16 +45,16 @@
40
45
  "hono": "^4.12.10",
41
46
  "rrule": "^2.8.1",
42
47
  "zod": "^4.3.6",
43
- "@voyantjs/availability": "0.28.1",
44
- "@voyantjs/core": "0.28.1",
45
- "@voyantjs/db": "0.28.1",
46
- "@voyantjs/hono": "0.28.1",
47
- "@voyantjs/products": "0.28.1"
48
+ "@voyantjs/availability": "0.29.0",
49
+ "@voyantjs/core": "0.29.0",
50
+ "@voyantjs/db": "0.29.0",
51
+ "@voyantjs/hono": "0.29.0",
52
+ "@voyantjs/products": "0.29.0"
48
53
  },
49
54
  "devDependencies": {
50
55
  "typescript": "^6.0.2",
51
- "@voyantjs/extras": "0.28.1",
52
- "@voyantjs/facilities": "0.28.1",
56
+ "@voyantjs/extras": "0.29.0",
57
+ "@voyantjs/facilities": "0.29.0",
53
58
  "@voyantjs/voyant-typescript-config": "0.1.0"
54
59
  },
55
60
  "files": [