@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.
Files changed (44) hide show
  1. package/README.md +38 -0
  2. package/dist/events.d.ts +38 -0
  3. package/dist/events.d.ts.map +1 -0
  4. package/dist/events.js +25 -0
  5. package/dist/index.d.ts +11 -0
  6. package/dist/index.d.ts.map +1 -0
  7. package/dist/index.js +16 -0
  8. package/dist/routes-shared.d.ts +14 -0
  9. package/dist/routes-shared.d.ts.map +1 -0
  10. package/dist/routes-shared.js +3 -0
  11. package/dist/routes.d.ts +345 -0
  12. package/dist/routes.d.ts.map +1 -0
  13. package/dist/routes.js +55 -0
  14. package/dist/schema.d.ts +655 -0
  15. package/dist/schema.d.ts.map +1 -0
  16. package/dist/schema.js +126 -0
  17. package/dist/service-booking-confirmed.d.ts +77 -0
  18. package/dist/service-booking-confirmed.d.ts.map +1 -0
  19. package/dist/service-booking-confirmed.js +134 -0
  20. package/dist/service-boundary-scheduler.d.ts +85 -0
  21. package/dist/service-boundary-scheduler.d.ts.map +1 -0
  22. package/dist/service-boundary-scheduler.js +141 -0
  23. package/dist/service-catalog-evaluator.d.ts +22 -0
  24. package/dist/service-catalog-evaluator.d.ts.map +1 -0
  25. package/dist/service-catalog-evaluator.js +33 -0
  26. package/dist/service-catalog-plane-promotions.d.ts +72 -0
  27. package/dist/service-catalog-plane-promotions.d.ts.map +1 -0
  28. package/dist/service-catalog-plane-promotions.js +119 -0
  29. package/dist/service-evaluator.d.ts +111 -0
  30. package/dist/service-evaluator.d.ts.map +1 -0
  31. package/dist/service-evaluator.js +264 -0
  32. package/dist/service-storefront.d.ts +40 -0
  33. package/dist/service-storefront.d.ts.map +1 -0
  34. package/dist/service-storefront.js +146 -0
  35. package/dist/service.d.ts +120 -0
  36. package/dist/service.d.ts.map +1 -0
  37. package/dist/service.js +296 -0
  38. package/dist/validation.d.ts +140 -0
  39. package/dist/validation.d.ts.map +1 -0
  40. package/dist/validation.js +134 -0
  41. package/dist/workflow-bulk-reindex.d.ts +55 -0
  42. package/dist/workflow-bulk-reindex.d.ts.map +1 -0
  43. package/dist/workflow-bulk-reindex.js +58 -0
  44. 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
+ }