@voyantjs/pricing 0.28.3 → 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.
- package/dist/events.d.ts +53 -0
- package/dist/events.d.ts.map +1 -0
- package/dist/events.js +28 -0
- package/dist/routes-core.d.ts +8 -8
- package/dist/routes-public.d.ts +1 -1
- package/dist/routes-rules.d.ts +1 -1
- package/dist/routes-rules.d.ts.map +1 -1
- package/dist/routes-rules.js +10 -6
- package/dist/routes-shared.d.ts +2 -0
- package/dist/routes-shared.d.ts.map +1 -1
- package/dist/schema-categories.d.ts +1 -1
- package/dist/service-catalog-plane-pricing.d.ts +80 -0
- package/dist/service-catalog-plane-pricing.d.ts.map +1 -0
- package/dist/service-catalog-plane-pricing.js +181 -0
- package/dist/service-catalogs.d.ts +2 -2
- package/dist/service-categories.d.ts +5 -5
- package/dist/service-option-rules.d.ts +21 -7
- package/dist/service-option-rules.d.ts.map +1 -1
- package/dist/service-option-rules.js +135 -13
- package/dist/service-policies.d.ts +1 -1
- package/dist/service-public.d.ts +1 -1
- package/dist/validation-public.d.ts +3 -3
- package/dist/validation-shared.d.ts +1 -1
- package/dist/validation.d.ts +4 -4
- package/package.json +13 -8
package/dist/events.d.ts
ADDED
|
@@ -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";
|
package/dist/routes-core.d.ts
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" | "
|
|
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" | "
|
|
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" | "
|
|
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" | "
|
|
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;
|
package/dist/routes-public.d.ts
CHANGED
|
@@ -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: "
|
|
113
|
+
status: "cancelled" | "open" | "closed" | "sold_out";
|
|
114
114
|
unlimited: boolean;
|
|
115
115
|
remainingPax: number | null;
|
|
116
116
|
remainingResources: number | null;
|
package/dist/routes-rules.d.ts
CHANGED
|
@@ -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;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
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"}
|
package/dist/routes-rules.js
CHANGED
|
@@ -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))))
|
package/dist/routes-shared.d.ts
CHANGED
|
@@ -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;
|
|
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" | "
|
|
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" | "
|
|
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" | "
|
|
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" | "
|
|
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" | "
|
|
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":"
|
|
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,6 +1,15 @@
|
|
|
1
1
|
import { RequestValidationError } from "@voyantjs/hono";
|
|
2
2
|
import { and, asc, desc, eq, sql } from "drizzle-orm";
|
|
3
|
+
import { PRICING_RULE_CHANGED_EVENT, } from "./events.js";
|
|
3
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
|
+
}
|
|
4
13
|
// A `per_booking` rule produces a single flat amount for the whole booking;
|
|
5
14
|
// per-unit prices implicitly assume a per-unit (or per-person) multiplier.
|
|
6
15
|
// The two are contradictory — see #482.
|
|
@@ -38,11 +47,19 @@ export async function getOptionPriceRuleById(db, id) {
|
|
|
38
47
|
const [row] = await db.select().from(optionPriceRules).where(eq(optionPriceRules.id, id)).limit(1);
|
|
39
48
|
return row ?? null;
|
|
40
49
|
}
|
|
41
|
-
export async function createOptionPriceRule(db, data) {
|
|
50
|
+
export async function createOptionPriceRule(db, data, runtime = {}) {
|
|
42
51
|
const [row] = await db.insert(optionPriceRules).values(data).returning();
|
|
43
|
-
|
|
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;
|
|
44
61
|
}
|
|
45
|
-
export async function updateOptionPriceRule(db, id, data) {
|
|
62
|
+
export async function updateOptionPriceRule(db, id, data, runtime = {}) {
|
|
46
63
|
if (data.pricingMode === "per_booking") {
|
|
47
64
|
const [countRow] = await db
|
|
48
65
|
.select({ count: sql `count(*)::int` })
|
|
@@ -56,19 +73,61 @@ export async function updateOptionPriceRule(db, id, data) {
|
|
|
56
73
|
});
|
|
57
74
|
}
|
|
58
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);
|
|
59
85
|
const [row] = await db
|
|
60
86
|
.update(optionPriceRules)
|
|
61
87
|
.set({ ...data, updatedAt: new Date() })
|
|
62
88
|
.where(eq(optionPriceRules.id, id))
|
|
63
89
|
.returning();
|
|
64
|
-
|
|
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;
|
|
65
107
|
}
|
|
66
|
-
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);
|
|
67
116
|
const [row] = await db
|
|
68
117
|
.delete(optionPriceRules)
|
|
69
118
|
.where(eq(optionPriceRules.id, id))
|
|
70
119
|
.returning({ id: optionPriceRules.id });
|
|
71
|
-
|
|
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;
|
|
72
131
|
}
|
|
73
132
|
export async function listOptionUnitPriceRules(db, query) {
|
|
74
133
|
const conditions = [];
|
|
@@ -101,9 +160,25 @@ export async function getOptionUnitPriceRuleById(db, id) {
|
|
|
101
160
|
.limit(1);
|
|
102
161
|
return row ?? null;
|
|
103
162
|
}
|
|
104
|
-
|
|
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 = {}) {
|
|
105
180
|
const [parent] = await db
|
|
106
|
-
.select({ pricingMode: optionPriceRules.pricingMode })
|
|
181
|
+
.select({ pricingMode: optionPriceRules.pricingMode, productId: optionPriceRules.productId })
|
|
107
182
|
.from(optionPriceRules)
|
|
108
183
|
.where(eq(optionPriceRules.id, data.optionPriceRuleId))
|
|
109
184
|
.limit(1);
|
|
@@ -113,22 +188,69 @@ export async function createOptionUnitPriceRule(db, data) {
|
|
|
113
188
|
});
|
|
114
189
|
}
|
|
115
190
|
const [row] = await db.insert(optionUnitPriceRules).values(data).returning();
|
|
116
|
-
|
|
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;
|
|
117
202
|
}
|
|
118
|
-
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);
|
|
119
210
|
const [row] = await db
|
|
120
211
|
.update(optionUnitPriceRules)
|
|
121
212
|
.set({ ...data, updatedAt: new Date() })
|
|
122
213
|
.where(eq(optionUnitPriceRules.id, id))
|
|
123
214
|
.returning();
|
|
124
|
-
|
|
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;
|
|
125
235
|
}
|
|
126
|
-
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);
|
|
127
239
|
const [row] = await db
|
|
128
240
|
.delete(optionUnitPriceRules)
|
|
129
241
|
.where(eq(optionUnitPriceRules.id, id))
|
|
130
242
|
.returning({ id: optionUnitPriceRules.id });
|
|
131
|
-
|
|
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;
|
|
132
254
|
}
|
|
133
255
|
export async function listOptionStartTimeRules(db, query) {
|
|
134
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;
|
package/dist/service-public.d.ts
CHANGED
|
@@ -68,7 +68,7 @@ export declare const publicPricingService: {
|
|
|
68
68
|
startsAt: string | null;
|
|
69
69
|
endsAt: string | null;
|
|
70
70
|
timezone: string;
|
|
71
|
-
status: "
|
|
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>;
|
package/dist/validation.d.ts
CHANGED
|
@@ -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.
|
|
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.
|
|
44
|
-
"@voyantjs/core": "0.
|
|
45
|
-
"@voyantjs/db": "0.
|
|
46
|
-
"@voyantjs/hono": "0.
|
|
47
|
-
"@voyantjs/products": "0.
|
|
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.
|
|
52
|
-
"@voyantjs/facilities": "0.
|
|
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": [
|