@voyantjs/promotions 0.28.3
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/README.md +38 -0
- package/dist/events.d.ts +38 -0
- package/dist/events.d.ts.map +1 -0
- package/dist/events.js +25 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +16 -0
- package/dist/routes-shared.d.ts +14 -0
- package/dist/routes-shared.d.ts.map +1 -0
- package/dist/routes-shared.js +3 -0
- package/dist/routes.d.ts +345 -0
- package/dist/routes.d.ts.map +1 -0
- package/dist/routes.js +55 -0
- package/dist/schema.d.ts +655 -0
- package/dist/schema.d.ts.map +1 -0
- package/dist/schema.js +126 -0
- package/dist/service-booking-confirmed.d.ts +77 -0
- package/dist/service-booking-confirmed.d.ts.map +1 -0
- package/dist/service-booking-confirmed.js +134 -0
- package/dist/service-boundary-scheduler.d.ts +85 -0
- package/dist/service-boundary-scheduler.d.ts.map +1 -0
- package/dist/service-boundary-scheduler.js +141 -0
- package/dist/service-catalog-evaluator.d.ts +22 -0
- package/dist/service-catalog-evaluator.d.ts.map +1 -0
- package/dist/service-catalog-evaluator.js +33 -0
- package/dist/service-catalog-plane-promotions.d.ts +72 -0
- package/dist/service-catalog-plane-promotions.d.ts.map +1 -0
- package/dist/service-catalog-plane-promotions.js +119 -0
- package/dist/service-evaluator.d.ts +111 -0
- package/dist/service-evaluator.d.ts.map +1 -0
- package/dist/service-evaluator.js +264 -0
- package/dist/service-storefront.d.ts +40 -0
- package/dist/service-storefront.d.ts.map +1 -0
- package/dist/service-storefront.js +146 -0
- package/dist/service.d.ts +120 -0
- package/dist/service.d.ts.map +1 -0
- package/dist/service.js +296 -0
- package/dist/validation.d.ts +140 -0
- package/dist/validation.d.ts.map +1 -0
- package/dist/validation.js +134 -0
- package/dist/workflow-bulk-reindex.d.ts +55 -0
- package/dist/workflow-bulk-reindex.d.ts.map +1 -0
- package/dist/workflow-bulk-reindex.js +58 -0
- package/package.json +120 -0
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Validation schemas for the promotions module.
|
|
3
|
+
*
|
|
4
|
+
* Per docs/architecture/promotions-architecture.md §3.2 (scope), §4.1 (offer
|
|
5
|
+
* fields), §11 (currency rules), §12.1 (conditions schema).
|
|
6
|
+
*
|
|
7
|
+
* The scope discriminated union is the source of truth for what an offer
|
|
8
|
+
* applies to; the materialized `promotional_offer_products` link table (§4.2)
|
|
9
|
+
* is rebuilt from it on every create / update.
|
|
10
|
+
*/
|
|
11
|
+
import { z } from "zod";
|
|
12
|
+
export declare const promotionalOfferScopeSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
13
|
+
kind: z.ZodLiteral<"global">;
|
|
14
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
15
|
+
kind: z.ZodLiteral<"products">;
|
|
16
|
+
productIds: z.ZodArray<z.ZodString>;
|
|
17
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
18
|
+
kind: z.ZodLiteral<"categories">;
|
|
19
|
+
categoryIds: z.ZodArray<z.ZodString>;
|
|
20
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
21
|
+
kind: z.ZodLiteral<"destinations">;
|
|
22
|
+
destinationIds: z.ZodArray<z.ZodString>;
|
|
23
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
24
|
+
kind: z.ZodLiteral<"markets">;
|
|
25
|
+
marketIds: z.ZodArray<z.ZodString>;
|
|
26
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
27
|
+
kind: z.ZodLiteral<"audiences">;
|
|
28
|
+
audiences: z.ZodArray<z.ZodEnum<{
|
|
29
|
+
staff: "staff";
|
|
30
|
+
customer: "customer";
|
|
31
|
+
partner: "partner";
|
|
32
|
+
supplier: "supplier";
|
|
33
|
+
}>>;
|
|
34
|
+
}, z.core.$strip>], "kind">;
|
|
35
|
+
export type PromotionalOfferScope = z.infer<typeof promotionalOfferScopeSchema>;
|
|
36
|
+
export type PromotionalOfferScopeKind = PromotionalOfferScope["kind"];
|
|
37
|
+
export declare const promotionalOfferConditionsSchema: z.ZodObject<{
|
|
38
|
+
minPax: z.ZodOptional<z.ZodNumber>;
|
|
39
|
+
}, z.core.$strip>;
|
|
40
|
+
export type PromotionalOfferConditions = z.infer<typeof promotionalOfferConditionsSchema>;
|
|
41
|
+
export declare const insertPromotionalOfferSchema: z.ZodObject<{
|
|
42
|
+
name: z.ZodString;
|
|
43
|
+
slug: z.ZodString;
|
|
44
|
+
description: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
45
|
+
discountType: z.ZodEnum<{
|
|
46
|
+
percentage: "percentage";
|
|
47
|
+
fixed_amount: "fixed_amount";
|
|
48
|
+
}>;
|
|
49
|
+
discountPercent: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
|
|
50
|
+
discountAmountCents: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
|
|
51
|
+
currency: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
52
|
+
scope: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
53
|
+
kind: z.ZodLiteral<"global">;
|
|
54
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
55
|
+
kind: z.ZodLiteral<"products">;
|
|
56
|
+
productIds: z.ZodArray<z.ZodString>;
|
|
57
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
58
|
+
kind: z.ZodLiteral<"categories">;
|
|
59
|
+
categoryIds: z.ZodArray<z.ZodString>;
|
|
60
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
61
|
+
kind: z.ZodLiteral<"destinations">;
|
|
62
|
+
destinationIds: z.ZodArray<z.ZodString>;
|
|
63
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
64
|
+
kind: z.ZodLiteral<"markets">;
|
|
65
|
+
marketIds: z.ZodArray<z.ZodString>;
|
|
66
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
67
|
+
kind: z.ZodLiteral<"audiences">;
|
|
68
|
+
audiences: z.ZodArray<z.ZodEnum<{
|
|
69
|
+
staff: "staff";
|
|
70
|
+
customer: "customer";
|
|
71
|
+
partner: "partner";
|
|
72
|
+
supplier: "supplier";
|
|
73
|
+
}>>;
|
|
74
|
+
}, z.core.$strip>], "kind">;
|
|
75
|
+
conditions: z.ZodDefault<z.ZodOptional<z.ZodObject<{
|
|
76
|
+
minPax: z.ZodOptional<z.ZodNumber>;
|
|
77
|
+
}, z.core.$strip>>>;
|
|
78
|
+
validFrom: z.ZodOptional<z.ZodNullable<z.ZodCoercedDate<unknown>>>;
|
|
79
|
+
validUntil: z.ZodOptional<z.ZodNullable<z.ZodCoercedDate<unknown>>>;
|
|
80
|
+
code: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
81
|
+
stackable: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
|
|
82
|
+
active: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
|
|
83
|
+
metadata: z.ZodOptional<z.ZodNullable<z.ZodRecord<z.ZodString, z.ZodUnknown>>>;
|
|
84
|
+
}, z.core.$strip>;
|
|
85
|
+
export declare const updatePromotionalOfferSchema: z.ZodObject<{
|
|
86
|
+
discountType: z.ZodOptional<z.ZodEnum<{
|
|
87
|
+
percentage: "percentage";
|
|
88
|
+
fixed_amount: "fixed_amount";
|
|
89
|
+
}>>;
|
|
90
|
+
scope: z.ZodOptional<z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
91
|
+
kind: z.ZodLiteral<"global">;
|
|
92
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
93
|
+
kind: z.ZodLiteral<"products">;
|
|
94
|
+
productIds: z.ZodArray<z.ZodString>;
|
|
95
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
96
|
+
kind: z.ZodLiteral<"categories">;
|
|
97
|
+
categoryIds: z.ZodArray<z.ZodString>;
|
|
98
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
99
|
+
kind: z.ZodLiteral<"destinations">;
|
|
100
|
+
destinationIds: z.ZodArray<z.ZodString>;
|
|
101
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
102
|
+
kind: z.ZodLiteral<"markets">;
|
|
103
|
+
marketIds: z.ZodArray<z.ZodString>;
|
|
104
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
105
|
+
kind: z.ZodLiteral<"audiences">;
|
|
106
|
+
audiences: z.ZodArray<z.ZodEnum<{
|
|
107
|
+
staff: "staff";
|
|
108
|
+
customer: "customer";
|
|
109
|
+
partner: "partner";
|
|
110
|
+
supplier: "supplier";
|
|
111
|
+
}>>;
|
|
112
|
+
}, z.core.$strip>], "kind">>;
|
|
113
|
+
name: z.ZodOptional<z.ZodString>;
|
|
114
|
+
slug: z.ZodOptional<z.ZodString>;
|
|
115
|
+
description: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
116
|
+
discountPercent: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
|
|
117
|
+
discountAmountCents: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
|
|
118
|
+
currency: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
119
|
+
conditions: z.ZodDefault<z.ZodOptional<z.ZodObject<{
|
|
120
|
+
minPax: z.ZodOptional<z.ZodNumber>;
|
|
121
|
+
}, z.core.$strip>>>;
|
|
122
|
+
validFrom: z.ZodOptional<z.ZodNullable<z.ZodCoercedDate<unknown>>>;
|
|
123
|
+
validUntil: z.ZodOptional<z.ZodNullable<z.ZodCoercedDate<unknown>>>;
|
|
124
|
+
code: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
125
|
+
stackable: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
|
|
126
|
+
active: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
|
|
127
|
+
metadata: z.ZodOptional<z.ZodNullable<z.ZodRecord<z.ZodString, z.ZodUnknown>>>;
|
|
128
|
+
}, z.core.$strip>;
|
|
129
|
+
export declare const promotionalOfferListQuerySchema: z.ZodObject<{
|
|
130
|
+
active: z.ZodOptional<z.ZodPipe<z.ZodUnion<readonly [z.ZodLiteral<"true">, z.ZodLiteral<"false">]>, z.ZodTransform<boolean, "true" | "false">>>;
|
|
131
|
+
code: z.ZodOptional<z.ZodString>;
|
|
132
|
+
limit: z.ZodDefault<z.ZodOptional<z.ZodCoercedNumber<unknown>>>;
|
|
133
|
+
offset: z.ZodDefault<z.ZodOptional<z.ZodCoercedNumber<unknown>>>;
|
|
134
|
+
}, z.core.$strip>;
|
|
135
|
+
export type InsertPromotionalOfferInput = z.input<typeof insertPromotionalOfferSchema>;
|
|
136
|
+
export type InsertPromotionalOffer = z.infer<typeof insertPromotionalOfferSchema>;
|
|
137
|
+
export type UpdatePromotionalOfferInput = z.input<typeof updatePromotionalOfferSchema>;
|
|
138
|
+
export type UpdatePromotionalOffer = z.infer<typeof updatePromotionalOfferSchema>;
|
|
139
|
+
export type PromotionalOfferListQuery = z.infer<typeof promotionalOfferListQuerySchema>;
|
|
140
|
+
//# sourceMappingURL=validation.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"validation.d.ts","sourceRoot":"","sources":["../src/validation.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAA;AAUvB,eAAO,MAAM,2BAA2B;;;;;;;;;;;;;;;;;;;;;;2BAsBtC,CAAA;AAEF,MAAM,MAAM,qBAAqB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,2BAA2B,CAAC,CAAA;AAC/E,MAAM,MAAM,yBAAyB,GAAG,qBAAqB,CAAC,MAAM,CAAC,CAAA;AAOrE,eAAO,MAAM,gCAAgC;;iBAK3C,CAAA;AAEF,MAAM,MAAM,0BAA0B,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,gCAAgC,CAAC,CAAA;AAqFzF,eAAO,MAAM,4BAA4B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAAmD,CAAA;AAE5F,eAAO,MAAM,4BAA4B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAQxC,CAAA;AAED,eAAO,MAAM,+BAA+B;;;;;iBAQ1C,CAAA;AAEF,MAAM,MAAM,2BAA2B,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,4BAA4B,CAAC,CAAA;AACtF,MAAM,MAAM,sBAAsB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,4BAA4B,CAAC,CAAA;AACjF,MAAM,MAAM,2BAA2B,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,4BAA4B,CAAC,CAAA;AACtF,MAAM,MAAM,sBAAsB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,4BAA4B,CAAC,CAAA;AACjF,MAAM,MAAM,yBAAyB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,+BAA+B,CAAC,CAAA"}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Validation schemas for the promotions module.
|
|
3
|
+
*
|
|
4
|
+
* Per docs/architecture/promotions-architecture.md §3.2 (scope), §4.1 (offer
|
|
5
|
+
* fields), §11 (currency rules), §12.1 (conditions schema).
|
|
6
|
+
*
|
|
7
|
+
* The scope discriminated union is the source of truth for what an offer
|
|
8
|
+
* applies to; the materialized `promotional_offer_products` link table (§4.2)
|
|
9
|
+
* is rebuilt from it on every create / update.
|
|
10
|
+
*/
|
|
11
|
+
import { z } from "zod";
|
|
12
|
+
// ---------- Scope discriminated union (§3.2) ----------
|
|
13
|
+
//
|
|
14
|
+
// Audience literal inlined to avoid a back-edge from @voyantjs/promotions to
|
|
15
|
+
// @voyantjs/catalog (where Visibility lives). A unit test pins the literal
|
|
16
|
+
// set against catalog's Visibility export.
|
|
17
|
+
const audienceLiteral = z.enum(["staff", "customer", "partner", "supplier"]);
|
|
18
|
+
export const promotionalOfferScopeSchema = z.discriminatedUnion("kind", [
|
|
19
|
+
z.object({ kind: z.literal("global") }),
|
|
20
|
+
z.object({
|
|
21
|
+
kind: z.literal("products"),
|
|
22
|
+
productIds: z.array(z.string().min(1)).min(1),
|
|
23
|
+
}),
|
|
24
|
+
z.object({
|
|
25
|
+
kind: z.literal("categories"),
|
|
26
|
+
categoryIds: z.array(z.string().min(1)).min(1),
|
|
27
|
+
}),
|
|
28
|
+
z.object({
|
|
29
|
+
kind: z.literal("destinations"),
|
|
30
|
+
destinationIds: z.array(z.string().min(1)).min(1),
|
|
31
|
+
}),
|
|
32
|
+
z.object({
|
|
33
|
+
kind: z.literal("markets"),
|
|
34
|
+
marketIds: z.array(z.string().min(1)).min(1),
|
|
35
|
+
}),
|
|
36
|
+
z.object({
|
|
37
|
+
kind: z.literal("audiences"),
|
|
38
|
+
audiences: z.array(audienceLiteral).min(1),
|
|
39
|
+
}),
|
|
40
|
+
]);
|
|
41
|
+
// ---------- Conditions (§12.1) ----------
|
|
42
|
+
//
|
|
43
|
+
// Typed JSONB validated by Zod. Date validity stays on the offer header
|
|
44
|
+
// (`validFrom` / `validUntil`) — not duplicated in `conditions` for v1.
|
|
45
|
+
export const promotionalOfferConditionsSchema = z.object({
|
|
46
|
+
/** Minimum total travelers. Catalog-plane evaluation surfaces this as a
|
|
47
|
+
* conditional offer when pax is unknown; checkout treats below-minimum
|
|
48
|
+
* as a hard exclusion. */
|
|
49
|
+
minPax: z.number().int().positive().optional(),
|
|
50
|
+
});
|
|
51
|
+
// ---------- Discount type / value cross-field rule (§11) ----------
|
|
52
|
+
//
|
|
53
|
+
// `percentage` requires `discountPercent`; `fixed_amount` requires
|
|
54
|
+
// `discountAmountCents` + `currency`. The other-flavor fields must be
|
|
55
|
+
// null/undefined to prevent operator confusion.
|
|
56
|
+
const discountTypeEnum = z.enum(["percentage", "fixed_amount"]);
|
|
57
|
+
const baseOfferShape = {
|
|
58
|
+
name: z.string().min(1).max(200),
|
|
59
|
+
slug: z
|
|
60
|
+
.string()
|
|
61
|
+
.min(1)
|
|
62
|
+
.max(200)
|
|
63
|
+
.regex(/^[a-z0-9-]+$/, "slug must be lowercase alphanumeric with hyphens"),
|
|
64
|
+
description: z.string().nullable().optional(),
|
|
65
|
+
discountType: discountTypeEnum,
|
|
66
|
+
discountPercent: z.number().positive().max(100).nullable().optional(),
|
|
67
|
+
discountAmountCents: z.number().int().positive().nullable().optional(),
|
|
68
|
+
currency: z
|
|
69
|
+
.string()
|
|
70
|
+
.length(3)
|
|
71
|
+
.regex(/^[A-Z]{3}$/, "currency must be a 3-letter ISO 4217 code")
|
|
72
|
+
.nullable()
|
|
73
|
+
.optional(),
|
|
74
|
+
scope: promotionalOfferScopeSchema,
|
|
75
|
+
conditions: promotionalOfferConditionsSchema.optional().default({}),
|
|
76
|
+
validFrom: z.coerce.date().nullable().optional(),
|
|
77
|
+
validUntil: z.coerce.date().nullable().optional(),
|
|
78
|
+
/** Stored lowercase. Provided in any case at the API; lowercased before insert. */
|
|
79
|
+
code: z
|
|
80
|
+
.string()
|
|
81
|
+
.min(1)
|
|
82
|
+
.max(80)
|
|
83
|
+
.regex(/^[A-Za-z0-9_-]+$/, "code must be alphanumeric (with - or _)")
|
|
84
|
+
.nullable()
|
|
85
|
+
.optional(),
|
|
86
|
+
stackable: z.boolean().optional().default(false),
|
|
87
|
+
active: z.boolean().optional().default(true),
|
|
88
|
+
metadata: z.record(z.string(), z.unknown()).nullable().optional(),
|
|
89
|
+
};
|
|
90
|
+
function applyDiscountTypeRules(schema) {
|
|
91
|
+
return schema
|
|
92
|
+
.refine((value) => {
|
|
93
|
+
if (value.discountType !== "percentage")
|
|
94
|
+
return true;
|
|
95
|
+
return (value.discountPercent != null &&
|
|
96
|
+
value.discountAmountCents == null &&
|
|
97
|
+
value.currency == null);
|
|
98
|
+
}, {
|
|
99
|
+
message: "percentage offers require `discountPercent` and must not set `discountAmountCents` or `currency`",
|
|
100
|
+
path: ["discountType"],
|
|
101
|
+
})
|
|
102
|
+
.refine((value) => {
|
|
103
|
+
if (value.discountType !== "fixed_amount")
|
|
104
|
+
return true;
|
|
105
|
+
return (value.discountAmountCents != null &&
|
|
106
|
+
value.currency != null &&
|
|
107
|
+
value.discountPercent == null);
|
|
108
|
+
}, {
|
|
109
|
+
message: "fixed_amount offers require `discountAmountCents` and `currency` and must not set `discountPercent`",
|
|
110
|
+
path: ["discountType"],
|
|
111
|
+
})
|
|
112
|
+
.refine((value) => {
|
|
113
|
+
if (value.validFrom == null || value.validUntil == null)
|
|
114
|
+
return true;
|
|
115
|
+
return value.validFrom < value.validUntil;
|
|
116
|
+
}, { message: "`validFrom` must be earlier than `validUntil`", path: ["validFrom"] });
|
|
117
|
+
}
|
|
118
|
+
export const insertPromotionalOfferSchema = applyDiscountTypeRules(z.object(baseOfferShape));
|
|
119
|
+
export const updatePromotionalOfferSchema = applyDiscountTypeRules(z.object({
|
|
120
|
+
...baseOfferShape,
|
|
121
|
+
discountType: discountTypeEnum.optional(),
|
|
122
|
+
scope: promotionalOfferScopeSchema.optional(),
|
|
123
|
+
name: baseOfferShape.name.optional(),
|
|
124
|
+
slug: baseOfferShape.slug.optional(),
|
|
125
|
+
}));
|
|
126
|
+
export const promotionalOfferListQuerySchema = z.object({
|
|
127
|
+
active: z
|
|
128
|
+
.union([z.literal("true"), z.literal("false")])
|
|
129
|
+
.transform((v) => v === "true")
|
|
130
|
+
.optional(),
|
|
131
|
+
code: z.string().min(1).max(80).optional(),
|
|
132
|
+
limit: z.coerce.number().int().positive().max(200).optional().default(50),
|
|
133
|
+
offset: z.coerce.number().int().nonnegative().optional().default(0),
|
|
134
|
+
});
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bulk-reindex workflow + event filter for `affected.kind === "all"`.
|
|
3
|
+
*
|
|
4
|
+
* The `promotion.changed` payload's `affected` is a discriminated union:
|
|
5
|
+
* - `{ kind: "products", productIds }` — small, bounded set; the catalog
|
|
6
|
+
* bridge reindexes inline on the in-process EventBus subscriber.
|
|
7
|
+
* - `{ kind: "all" }` — every owned product (global / market / audience-
|
|
8
|
+
* scoped offers). Inline enumeration would burn the request handler's
|
|
9
|
+
* CPU budget on a sizeable catalog, so this branch routes through a
|
|
10
|
+
* workflow that breaks the work into one step per product. The
|
|
11
|
+
* orchestrator schedules them in parallel so each individual step stays
|
|
12
|
+
* inside Worker CPU limits.
|
|
13
|
+
*
|
|
14
|
+
* The workflow body delegates catalog access to a service the operator
|
|
15
|
+
* template registers under `BULK_REINDEX_SERVICE_KEY`. The promotions
|
|
16
|
+
* package stays catalog-agnostic: it knows nothing about Typesense / index
|
|
17
|
+
* slices / document builders. The seam is the same one used elsewhere in
|
|
18
|
+
* the codebase for cross-module behavior (workflow → ctx.services.resolve).
|
|
19
|
+
*/
|
|
20
|
+
import { type PromotionChangedSource } from "./events.js";
|
|
21
|
+
/**
|
|
22
|
+
* Service-container key the operator template registers a concrete
|
|
23
|
+
* implementation against. Kept stable + exported so consumers don't have
|
|
24
|
+
* to repeat the magic string.
|
|
25
|
+
*/
|
|
26
|
+
export declare const BULK_REINDEX_SERVICE_KEY: "promotions:bulk-reindex-products";
|
|
27
|
+
/**
|
|
28
|
+
* Contract the operator template implements. The workflow body resolves
|
|
29
|
+
* this from `ctx.services` and calls into it from steps.
|
|
30
|
+
*
|
|
31
|
+
* Two methods so the workflow can split enumeration from per-product
|
|
32
|
+
* reindex — that's what makes the parallel-step pattern viable on edge
|
|
33
|
+
* runtime. A single `reindexAllProducts()` would have to do everything in
|
|
34
|
+
* one step, which is what we're trying to avoid.
|
|
35
|
+
*/
|
|
36
|
+
export interface BulkReindexProductsService {
|
|
37
|
+
listAllProductIds(): Promise<string[]>;
|
|
38
|
+
reindexProduct(productId: string): Promise<void>;
|
|
39
|
+
}
|
|
40
|
+
export interface BulkReindexProductsInput {
|
|
41
|
+
/** The offer that triggered the reindex (for logging / correlation). */
|
|
42
|
+
offerId: string;
|
|
43
|
+
source: PromotionChangedSource;
|
|
44
|
+
}
|
|
45
|
+
export interface BulkReindexProductsOutput {
|
|
46
|
+
reindexed: number;
|
|
47
|
+
}
|
|
48
|
+
export declare const bulkReindexProductsWorkflow: import("@voyantjs/workflows").WorkflowDefinition<BulkReindexProductsInput, BulkReindexProductsOutput>;
|
|
49
|
+
/**
|
|
50
|
+
* Routes `promotion.changed` envelopes whose `affected.kind === "all"` into
|
|
51
|
+
* the workflow above. Other shapes (`{ kind: "products", productIds }`) fall
|
|
52
|
+
* through to the in-process catalog-bridge subscriber.
|
|
53
|
+
*/
|
|
54
|
+
export declare const promotionAffectedAllFilter: import("@voyantjs/workflows/events").EventFilterRuntimeEntry;
|
|
55
|
+
//# sourceMappingURL=workflow-bulk-reindex.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"workflow-bulk-reindex.d.ts","sourceRoot":"","sources":["../src/workflow-bulk-reindex.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAIH,OAAO,EAA2B,KAAK,sBAAsB,EAAE,MAAM,aAAa,CAAA;AAElF;;;;GAIG;AACH,eAAO,MAAM,wBAAwB,EAAG,kCAA2C,CAAA;AAEnF;;;;;;;;GAQG;AACH,MAAM,WAAW,0BAA0B;IACzC,iBAAiB,IAAI,OAAO,CAAC,MAAM,EAAE,CAAC,CAAA;IACtC,cAAc,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;CACjD;AAED,MAAM,WAAW,wBAAwB;IACvC,wEAAwE;IACxE,OAAO,EAAE,MAAM,CAAA;IACf,MAAM,EAAE,sBAAsB,CAAA;CAC/B;AAED,MAAM,WAAW,yBAAyB;IACxC,SAAS,EAAE,MAAM,CAAA;CAClB;AAKD,eAAO,MAAM,2BAA2B,uGAuBtC,CAAA;AAEF;;;;GAIG;AACH,eAAO,MAAM,0BAA0B,8DAYtC,CAAA"}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bulk-reindex workflow + event filter for `affected.kind === "all"`.
|
|
3
|
+
*
|
|
4
|
+
* The `promotion.changed` payload's `affected` is a discriminated union:
|
|
5
|
+
* - `{ kind: "products", productIds }` — small, bounded set; the catalog
|
|
6
|
+
* bridge reindexes inline on the in-process EventBus subscriber.
|
|
7
|
+
* - `{ kind: "all" }` — every owned product (global / market / audience-
|
|
8
|
+
* scoped offers). Inline enumeration would burn the request handler's
|
|
9
|
+
* CPU budget on a sizeable catalog, so this branch routes through a
|
|
10
|
+
* workflow that breaks the work into one step per product. The
|
|
11
|
+
* orchestrator schedules them in parallel so each individual step stays
|
|
12
|
+
* inside Worker CPU limits.
|
|
13
|
+
*
|
|
14
|
+
* The workflow body delegates catalog access to a service the operator
|
|
15
|
+
* template registers under `BULK_REINDEX_SERVICE_KEY`. The promotions
|
|
16
|
+
* package stays catalog-agnostic: it knows nothing about Typesense / index
|
|
17
|
+
* slices / document builders. The seam is the same one used elsewhere in
|
|
18
|
+
* the codebase for cross-module behavior (workflow → ctx.services.resolve).
|
|
19
|
+
*/
|
|
20
|
+
import { trigger, workflow } from "@voyantjs/workflows";
|
|
21
|
+
import { PROMOTION_CHANGED_EVENT } from "./events.js";
|
|
22
|
+
/**
|
|
23
|
+
* Service-container key the operator template registers a concrete
|
|
24
|
+
* implementation against. Kept stable + exported so consumers don't have
|
|
25
|
+
* to repeat the magic string.
|
|
26
|
+
*/
|
|
27
|
+
export const BULK_REINDEX_SERVICE_KEY = "promotions:bulk-reindex-products";
|
|
28
|
+
/** Cap on concurrent per-product reindex steps to avoid hammering the index. */
|
|
29
|
+
const REINDEX_CONCURRENCY = 8;
|
|
30
|
+
export const bulkReindexProductsWorkflow = workflow({
|
|
31
|
+
id: "promotions.reindex-all-products",
|
|
32
|
+
defaultRuntime: "edge",
|
|
33
|
+
async run(_input, ctx) {
|
|
34
|
+
const svc = ctx.services.resolve(BULK_REINDEX_SERVICE_KEY);
|
|
35
|
+
const ids = await ctx.step("list-product-ids", async () => svc.listAllProductIds());
|
|
36
|
+
if (ids.length === 0)
|
|
37
|
+
return { reindexed: 0 };
|
|
38
|
+
await ctx.parallel(ids, async (productId) => ctx.step(`reindex:${productId}`, async () => {
|
|
39
|
+
await svc.reindexProduct(productId);
|
|
40
|
+
}), { concurrency: REINDEX_CONCURRENCY });
|
|
41
|
+
return { reindexed: ids.length };
|
|
42
|
+
},
|
|
43
|
+
});
|
|
44
|
+
/**
|
|
45
|
+
* Routes `promotion.changed` envelopes whose `affected.kind === "all"` into
|
|
46
|
+
* the workflow above. Other shapes (`{ kind: "products", productIds }`) fall
|
|
47
|
+
* through to the in-process catalog-bridge subscriber.
|
|
48
|
+
*/
|
|
49
|
+
export const promotionAffectedAllFilter = trigger.on(PROMOTION_CHANGED_EVENT, {
|
|
50
|
+
target: bulkReindexProductsWorkflow,
|
|
51
|
+
where: { eq: [{ path: "data.affected.kind" }, { lit: "all" }] },
|
|
52
|
+
input: {
|
|
53
|
+
object: {
|
|
54
|
+
offerId: { path: "data.offerId" },
|
|
55
|
+
source: { path: "data.source" },
|
|
56
|
+
},
|
|
57
|
+
},
|
|
58
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@voyantjs/promotions",
|
|
3
|
+
"version": "0.28.3",
|
|
4
|
+
"license": "Apache-2.0",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"exports": {
|
|
7
|
+
".": "./src/index.ts",
|
|
8
|
+
"./schema": "./src/schema.ts",
|
|
9
|
+
"./validation": "./src/validation.ts",
|
|
10
|
+
"./routes": "./src/routes.ts",
|
|
11
|
+
"./events": "./src/events.ts",
|
|
12
|
+
"./service": "./src/service.ts",
|
|
13
|
+
"./service-catalog-plane-promotions": "./src/service-catalog-plane-promotions.ts",
|
|
14
|
+
"./service-boundary-scheduler": "./src/service-boundary-scheduler.ts",
|
|
15
|
+
"./service-catalog-evaluator": "./src/service-catalog-evaluator.ts",
|
|
16
|
+
"./service-booking-confirmed": "./src/service-booking-confirmed.ts",
|
|
17
|
+
"./service-storefront": "./src/service-storefront.ts"
|
|
18
|
+
},
|
|
19
|
+
"scripts": {
|
|
20
|
+
"typecheck": "tsc --noEmit",
|
|
21
|
+
"lint": "biome check src/",
|
|
22
|
+
"test": "vitest run",
|
|
23
|
+
"build": "tsc -p tsconfig.json",
|
|
24
|
+
"clean": "rm -rf dist tsconfig.tsbuildinfo",
|
|
25
|
+
"prepack": "pnpm run build"
|
|
26
|
+
},
|
|
27
|
+
"dependencies": {
|
|
28
|
+
"@voyantjs/catalog": "workspace:*",
|
|
29
|
+
"@voyantjs/core": "workspace:*",
|
|
30
|
+
"@voyantjs/db": "workspace:*",
|
|
31
|
+
"@voyantjs/hono": "workspace:*",
|
|
32
|
+
"@voyantjs/products": "workspace:*",
|
|
33
|
+
"@voyantjs/storefront": "workspace:*",
|
|
34
|
+
"@voyantjs/workflows": "workspace:*",
|
|
35
|
+
"drizzle-orm": "^0.45.2",
|
|
36
|
+
"hono": "^4.12.10",
|
|
37
|
+
"zod": "^4.3.6"
|
|
38
|
+
},
|
|
39
|
+
"devDependencies": {
|
|
40
|
+
"@voyantjs/voyant-test-utils": "workspace:*",
|
|
41
|
+
"@voyantjs/voyant-typescript-config": "workspace:*",
|
|
42
|
+
"typescript": "^6.0.2"
|
|
43
|
+
},
|
|
44
|
+
"files": [
|
|
45
|
+
"dist"
|
|
46
|
+
],
|
|
47
|
+
"publishConfig": {
|
|
48
|
+
"access": "public",
|
|
49
|
+
"exports": {
|
|
50
|
+
".": {
|
|
51
|
+
"types": "./dist/index.d.ts",
|
|
52
|
+
"import": "./dist/index.js",
|
|
53
|
+
"default": "./dist/index.js"
|
|
54
|
+
},
|
|
55
|
+
"./schema": {
|
|
56
|
+
"types": "./dist/schema.d.ts",
|
|
57
|
+
"import": "./dist/schema.js",
|
|
58
|
+
"default": "./dist/schema.js"
|
|
59
|
+
},
|
|
60
|
+
"./validation": {
|
|
61
|
+
"types": "./dist/validation.d.ts",
|
|
62
|
+
"import": "./dist/validation.js",
|
|
63
|
+
"default": "./dist/validation.js"
|
|
64
|
+
},
|
|
65
|
+
"./routes": {
|
|
66
|
+
"types": "./dist/routes.d.ts",
|
|
67
|
+
"import": "./dist/routes.js",
|
|
68
|
+
"default": "./dist/routes.js"
|
|
69
|
+
},
|
|
70
|
+
"./events": {
|
|
71
|
+
"types": "./dist/events.d.ts",
|
|
72
|
+
"import": "./dist/events.js",
|
|
73
|
+
"default": "./dist/events.js"
|
|
74
|
+
},
|
|
75
|
+
"./service": {
|
|
76
|
+
"types": "./dist/service.d.ts",
|
|
77
|
+
"import": "./dist/service.js",
|
|
78
|
+
"default": "./dist/service.js"
|
|
79
|
+
},
|
|
80
|
+
"./service-catalog-plane-promotions": {
|
|
81
|
+
"types": "./dist/service-catalog-plane-promotions.d.ts",
|
|
82
|
+
"import": "./dist/service-catalog-plane-promotions.js",
|
|
83
|
+
"default": "./dist/service-catalog-plane-promotions.js"
|
|
84
|
+
},
|
|
85
|
+
"./service-boundary-scheduler": {
|
|
86
|
+
"types": "./dist/service-boundary-scheduler.d.ts",
|
|
87
|
+
"import": "./dist/service-boundary-scheduler.js",
|
|
88
|
+
"default": "./dist/service-boundary-scheduler.js"
|
|
89
|
+
},
|
|
90
|
+
"./service-catalog-evaluator": {
|
|
91
|
+
"types": "./dist/service-catalog-evaluator.d.ts",
|
|
92
|
+
"import": "./dist/service-catalog-evaluator.js",
|
|
93
|
+
"default": "./dist/service-catalog-evaluator.js"
|
|
94
|
+
},
|
|
95
|
+
"./service-booking-confirmed": {
|
|
96
|
+
"types": "./dist/service-booking-confirmed.d.ts",
|
|
97
|
+
"import": "./dist/service-booking-confirmed.js",
|
|
98
|
+
"default": "./dist/service-booking-confirmed.js"
|
|
99
|
+
},
|
|
100
|
+
"./service-storefront": {
|
|
101
|
+
"types": "./dist/service-storefront.d.ts",
|
|
102
|
+
"import": "./dist/service-storefront.js",
|
|
103
|
+
"default": "./dist/service-storefront.js"
|
|
104
|
+
}
|
|
105
|
+
},
|
|
106
|
+
"main": "./dist/index.js",
|
|
107
|
+
"types": "./dist/index.d.ts"
|
|
108
|
+
},
|
|
109
|
+
"repository": {
|
|
110
|
+
"type": "git",
|
|
111
|
+
"url": "https://github.com/voyantjs/voyant.git",
|
|
112
|
+
"directory": "packages/promotions"
|
|
113
|
+
},
|
|
114
|
+
"voyant": {
|
|
115
|
+
"schema": "./schema",
|
|
116
|
+
"requiresSchemas": [
|
|
117
|
+
"@voyantjs/db"
|
|
118
|
+
]
|
|
119
|
+
}
|
|
120
|
+
}
|