@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
package/README.md ADDED
@@ -0,0 +1,38 @@
1
+ # @voyantjs/promotions
2
+
3
+ Promotional offers for Voyant — auto-applied catalog discounts (badges, strikethrough prices), code-redeemed discounts at checkout, and audience- / market-scoped blanket discounts. Resolves issue #497.
4
+
5
+ PR1 ships the schema + admin CRUD only. Catalog plane wiring lands in PR3, booking-engine integration in PR4. See `docs/architecture/promotions-architecture.md` for the full design.
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ pnpm add @voyantjs/promotions
11
+ ```
12
+
13
+ ## Usage
14
+
15
+ ```typescript
16
+ import { promotionsModule } from "@voyantjs/promotions"
17
+ import { createApp } from "@voyantjs/hono"
18
+
19
+ const app = createApp({
20
+ modules: [promotionsModule],
21
+ // ...
22
+ })
23
+ ```
24
+
25
+ ## Exports
26
+
27
+ | Entry | Description |
28
+ | --- | --- |
29
+ | `.` | Module export (`promotionsModule`, `promotionsHonoModule`) |
30
+ | `./schema` | Drizzle tables (`promotionalOffers`, `promotionalOfferProducts`, `promotionalOfferRedemptions`) |
31
+ | `./validation` | Zod schemas (insert / update / scope discriminator / conditions) |
32
+ | `./routes` | Hono admin routes mounted at `/v1/admin/promotions/*` |
33
+ | `./events` | `PROMOTION_CHANGED_EVENT` + payload types |
34
+ | `./service` | `promotionsService` (CRUD + scope materialization) |
35
+
36
+ ## License
37
+
38
+ Apache-2.0
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Promotions domain events.
3
+ *
4
+ * Per docs/architecture/promotions-architecture.md §9.1.
5
+ *
6
+ * Emitted (after the service mutation commits) by every CRUD path that
7
+ * changes a field affecting projection or evaluation. Pure metadata-only
8
+ * edits (`description`, `metadata`) skip emission to avoid pointless
9
+ * reindex churn.
10
+ *
11
+ * The catalog bridge subscribes and dispatches per `affected.kind`:
12
+ * - `products` → reindex the listed product IDs only.
13
+ * - `all` → reindex every owned product (used when the resolved
14
+ * product set would be too large to enumerate, or for
15
+ * `global`-scope offers).
16
+ *
17
+ * No `slices` variant: `IndexerService` (packages/catalog/src/services/
18
+ * indexer-service.ts:75) has `reindexEntity` (one entity, all slices) and
19
+ * `reindexEntityForSlice` (one entity, one slice) but no "reindex every
20
+ * product in this slice" helper. Slice-shaped scopes (`markets`,
21
+ * `audiences`) are resolved to product IDs at emission time, falling back
22
+ * to `all` when the resolved set would be unbounded.
23
+ */
24
+ /** Stable string identifier for the event. */
25
+ export declare const PROMOTION_CHANGED_EVENT: "promotion.changed";
26
+ export type PromotionChangedSource = "created" | "updated" | "deleted" | "expired";
27
+ export type PromotionChangedAffected = {
28
+ kind: "products";
29
+ productIds: string[];
30
+ } | {
31
+ kind: "all";
32
+ };
33
+ export interface PromotionChangedEvent {
34
+ offerId: string;
35
+ source: PromotionChangedSource;
36
+ affected: PromotionChangedAffected;
37
+ }
38
+ //# sourceMappingURL=events.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"events.d.ts","sourceRoot":"","sources":["../src/events.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH,8CAA8C;AAC9C,eAAO,MAAM,uBAAuB,EAAG,mBAA4B,CAAA;AAEnE,MAAM,MAAM,sBAAsB,GAAG,SAAS,GAAG,SAAS,GAAG,SAAS,GAAG,SAAS,CAAA;AAElF,MAAM,MAAM,wBAAwB,GAAG;IAAE,IAAI,EAAE,UAAU,CAAC;IAAC,UAAU,EAAE,MAAM,EAAE,CAAA;CAAE,GAAG;IAAE,IAAI,EAAE,KAAK,CAAA;CAAE,CAAA;AAEnG,MAAM,WAAW,qBAAqB;IACpC,OAAO,EAAE,MAAM,CAAA;IACf,MAAM,EAAE,sBAAsB,CAAA;IAC9B,QAAQ,EAAE,wBAAwB,CAAA;CACnC"}
package/dist/events.js ADDED
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Promotions domain events.
3
+ *
4
+ * Per docs/architecture/promotions-architecture.md §9.1.
5
+ *
6
+ * Emitted (after the service mutation commits) by every CRUD path that
7
+ * changes a field affecting projection or evaluation. Pure metadata-only
8
+ * edits (`description`, `metadata`) skip emission to avoid pointless
9
+ * reindex churn.
10
+ *
11
+ * The catalog bridge subscribes and dispatches per `affected.kind`:
12
+ * - `products` → reindex the listed product IDs only.
13
+ * - `all` → reindex every owned product (used when the resolved
14
+ * product set would be too large to enumerate, or for
15
+ * `global`-scope offers).
16
+ *
17
+ * No `slices` variant: `IndexerService` (packages/catalog/src/services/
18
+ * indexer-service.ts:75) has `reindexEntity` (one entity, all slices) and
19
+ * `reindexEntityForSlice` (one entity, one slice) but no "reindex every
20
+ * product in this slice" helper. Slice-shaped scopes (`markets`,
21
+ * `audiences`) are resolved to product IDs at emission time, falling back
22
+ * to `all` when the resolved set would be unbounded.
23
+ */
24
+ /** Stable string identifier for the event. */
25
+ export const PROMOTION_CHANGED_EVENT = "promotion.changed";
@@ -0,0 +1,11 @@
1
+ import type { Module } from "@voyantjs/core";
2
+ import type { HonoModule } from "@voyantjs/hono/module";
3
+ export type { PromotionsRoutes } from "./routes.js";
4
+ export declare const promotionsModule: Module;
5
+ export declare const promotionsHonoModule: HonoModule;
6
+ export { PROMOTION_CHANGED_EVENT, type PromotionChangedAffected, type PromotionChangedEvent, type PromotionChangedSource, } from "./events.js";
7
+ export { type NewPromotionalOffer, type NewPromotionalOfferProduct, type NewPromotionalOfferRedemption, type PromotionalOffer, type PromotionalOfferDiscountType, type PromotionalOfferProduct, type PromotionalOfferRedemption, promotionalOfferDiscountTypeEnum, promotionalOfferProducts, promotionalOfferRedemptions, promotionalOffers, } from "./schema.js";
8
+ export { type OfferMutationRuntime, type PromotionsService, promotionsService, recomputeOfferLinks, resolveScopeProductIds, } from "./service.js";
9
+ export { type InsertPromotionalOffer, type InsertPromotionalOfferInput, insertPromotionalOfferSchema, type PromotionalOfferConditions, type PromotionalOfferListQuery, type PromotionalOfferScope, type PromotionalOfferScopeKind, promotionalOfferConditionsSchema, promotionalOfferListQuerySchema, promotionalOfferScopeSchema, type UpdatePromotionalOffer, type UpdatePromotionalOfferInput, updatePromotionalOfferSchema, } from "./validation.js";
10
+ export { BULK_REINDEX_SERVICE_KEY, type BulkReindexProductsInput, type BulkReindexProductsOutput, type BulkReindexProductsService, bulkReindexProductsWorkflow, promotionAffectedAllFilter, } from "./workflow-bulk-reindex.js";
11
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,gBAAgB,CAAA;AAC5C,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,uBAAuB,CAAA;AAKvD,YAAY,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAA;AAEnD,eAAO,MAAM,gBAAgB,EAAE,MAI9B,CAAA;AAED,eAAO,MAAM,oBAAoB,EAAE,UAGlC,CAAA;AAED,OAAO,EACL,uBAAuB,EACvB,KAAK,wBAAwB,EAC7B,KAAK,qBAAqB,EAC1B,KAAK,sBAAsB,GAC5B,MAAM,aAAa,CAAA;AAEpB,OAAO,EACL,KAAK,mBAAmB,EACxB,KAAK,0BAA0B,EAC/B,KAAK,6BAA6B,EAClC,KAAK,gBAAgB,EACrB,KAAK,4BAA4B,EACjC,KAAK,uBAAuB,EAC5B,KAAK,0BAA0B,EAC/B,gCAAgC,EAChC,wBAAwB,EACxB,2BAA2B,EAC3B,iBAAiB,GAClB,MAAM,aAAa,CAAA;AACpB,OAAO,EACL,KAAK,oBAAoB,EACzB,KAAK,iBAAiB,EACtB,iBAAiB,EACjB,mBAAmB,EACnB,sBAAsB,GACvB,MAAM,cAAc,CAAA;AACrB,OAAO,EACL,KAAK,sBAAsB,EAC3B,KAAK,2BAA2B,EAChC,4BAA4B,EAC5B,KAAK,0BAA0B,EAC/B,KAAK,yBAAyB,EAC9B,KAAK,qBAAqB,EAC1B,KAAK,yBAAyB,EAC9B,gCAAgC,EAChC,+BAA+B,EAC/B,2BAA2B,EAC3B,KAAK,sBAAsB,EAC3B,KAAK,2BAA2B,EAChC,4BAA4B,GAC7B,MAAM,iBAAiB,CAAA;AACxB,OAAO,EACL,wBAAwB,EACxB,KAAK,wBAAwB,EAC7B,KAAK,yBAAyB,EAC9B,KAAK,0BAA0B,EAC/B,2BAA2B,EAC3B,0BAA0B,GAC3B,MAAM,4BAA4B,CAAA"}
package/dist/index.js ADDED
@@ -0,0 +1,16 @@
1
+ import { promotionsRoutes } from "./routes.js";
2
+ import { bulkReindexProductsWorkflow, promotionAffectedAllFilter } from "./workflow-bulk-reindex.js";
3
+ export const promotionsModule = {
4
+ name: "promotions",
5
+ workflows: [bulkReindexProductsWorkflow],
6
+ eventFilters: [promotionAffectedAllFilter],
7
+ };
8
+ export const promotionsHonoModule = {
9
+ module: promotionsModule,
10
+ adminRoutes: promotionsRoutes,
11
+ };
12
+ export { PROMOTION_CHANGED_EVENT, } from "./events.js";
13
+ export { promotionalOfferDiscountTypeEnum, promotionalOfferProducts, promotionalOfferRedemptions, promotionalOffers, } from "./schema.js";
14
+ export { promotionsService, recomputeOfferLinks, resolveScopeProductIds, } from "./service.js";
15
+ export { insertPromotionalOfferSchema, promotionalOfferConditionsSchema, promotionalOfferListQuerySchema, promotionalOfferScopeSchema, updatePromotionalOfferSchema, } from "./validation.js";
16
+ export { BULK_REINDEX_SERVICE_KEY, bulkReindexProductsWorkflow, promotionAffectedAllFilter, } from "./workflow-bulk-reindex.js";
@@ -0,0 +1,14 @@
1
+ import type { EventBus } from "@voyantjs/core";
2
+ import type { PostgresJsDatabase } from "drizzle-orm/postgres-js";
3
+ import type { Context } from "hono";
4
+ export type Env = {
5
+ Variables: {
6
+ db: PostgresJsDatabase;
7
+ userId?: string;
8
+ eventBus?: EventBus;
9
+ };
10
+ };
11
+ export declare function notFound(c: Context<Env>, message: string): Response & import("hono").TypedResponse<{
12
+ error: string;
13
+ }, 404, "json">;
14
+ //# sourceMappingURL=routes-shared.d.ts.map
@@ -0,0 +1 @@
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"}
@@ -0,0 +1,3 @@
1
+ export function notFound(c, message) {
2
+ return c.json({ error: message }, 404);
3
+ }
@@ -0,0 +1,345 @@
1
+ /**
2
+ * Admin routes for promotions — mounted by the operator template at
3
+ * `/v1/admin/promotions/*` (staff-actor-gated by the parent app's
4
+ * middleware chain).
5
+ *
6
+ * PR1 ships CRUD only. Public storefront routes are exposed via the
7
+ * existing `/v1/public/products/:productId/offers` endpoints in
8
+ * `@voyantjs/storefront` once the storefront resolver is wired in PR4.
9
+ */
10
+ import { type Env } from "./routes-shared.js";
11
+ export declare const promotionsRoutes: import("hono/hono-base").HonoBase<Env, {
12
+ "/": {
13
+ $get: {
14
+ input: {};
15
+ output: {
16
+ data: {
17
+ id: string;
18
+ name: string;
19
+ slug: string;
20
+ description: string | null;
21
+ discountType: "percentage" | "fixed_amount";
22
+ discountPercent: string | null;
23
+ discountAmountCents: number | null;
24
+ currency: string | null;
25
+ scope: {
26
+ kind: "global";
27
+ } | {
28
+ kind: "products";
29
+ productIds: string[];
30
+ } | {
31
+ kind: "categories";
32
+ categoryIds: string[];
33
+ } | {
34
+ kind: "destinations";
35
+ destinationIds: string[];
36
+ } | {
37
+ kind: "markets";
38
+ marketIds: string[];
39
+ } | {
40
+ kind: "audiences";
41
+ audiences: ("staff" | "customer" | "partner" | "supplier")[];
42
+ };
43
+ conditions: {
44
+ minPax?: number | undefined;
45
+ };
46
+ validFrom: string | null;
47
+ validUntil: string | null;
48
+ code: string | null;
49
+ stackable: boolean;
50
+ active: boolean;
51
+ metadata: import("hono/utils/types").JSONValue;
52
+ createdAt: string;
53
+ updatedAt: string;
54
+ }[];
55
+ total: number;
56
+ limit: number;
57
+ offset: number;
58
+ };
59
+ outputFormat: "json";
60
+ status: import("hono/utils/http-status").ContentfulStatusCode;
61
+ };
62
+ };
63
+ } & {
64
+ "/": {
65
+ $post: {
66
+ input: {};
67
+ output: {
68
+ data: {
69
+ active: boolean;
70
+ code: string | null;
71
+ metadata: import("hono/utils/types").JSONValue;
72
+ id: string;
73
+ description: string | null;
74
+ name: string;
75
+ slug: string;
76
+ discountType: "percentage" | "fixed_amount";
77
+ discountPercent: string | null;
78
+ discountAmountCents: number | null;
79
+ currency: string | null;
80
+ scope: {
81
+ kind: "global";
82
+ } | {
83
+ kind: "products";
84
+ productIds: string[];
85
+ } | {
86
+ kind: "categories";
87
+ categoryIds: string[];
88
+ } | {
89
+ kind: "destinations";
90
+ destinationIds: string[];
91
+ } | {
92
+ kind: "markets";
93
+ marketIds: string[];
94
+ } | {
95
+ kind: "audiences";
96
+ audiences: ("staff" | "customer" | "partner" | "supplier")[];
97
+ };
98
+ conditions: {
99
+ minPax?: number | undefined;
100
+ };
101
+ validFrom: string | null;
102
+ validUntil: string | null;
103
+ stackable: boolean;
104
+ createdAt: string;
105
+ updatedAt: string;
106
+ };
107
+ };
108
+ outputFormat: "json";
109
+ status: 201;
110
+ };
111
+ };
112
+ } & {
113
+ "/:id": {
114
+ $get: {
115
+ input: {
116
+ param: {
117
+ id: string;
118
+ };
119
+ };
120
+ output: {
121
+ data: {
122
+ active: boolean;
123
+ code: string | null;
124
+ metadata: import("hono/utils/types").JSONValue;
125
+ id: string;
126
+ description: string | null;
127
+ name: string;
128
+ slug: string;
129
+ discountType: "percentage" | "fixed_amount";
130
+ discountPercent: string | null;
131
+ discountAmountCents: number | null;
132
+ currency: string | null;
133
+ scope: {
134
+ kind: "global";
135
+ } | {
136
+ kind: "products";
137
+ productIds: string[];
138
+ } | {
139
+ kind: "categories";
140
+ categoryIds: string[];
141
+ } | {
142
+ kind: "destinations";
143
+ destinationIds: string[];
144
+ } | {
145
+ kind: "markets";
146
+ marketIds: string[];
147
+ } | {
148
+ kind: "audiences";
149
+ audiences: ("staff" | "customer" | "partner" | "supplier")[];
150
+ };
151
+ conditions: {
152
+ minPax?: number | undefined;
153
+ };
154
+ validFrom: string | null;
155
+ validUntil: string | null;
156
+ stackable: boolean;
157
+ createdAt: string;
158
+ updatedAt: string;
159
+ };
160
+ };
161
+ outputFormat: "json";
162
+ status: import("hono/utils/http-status").ContentfulStatusCode;
163
+ } | {
164
+ input: {
165
+ param: {
166
+ id: string;
167
+ };
168
+ };
169
+ output: {
170
+ error: string;
171
+ };
172
+ outputFormat: "json";
173
+ status: 404;
174
+ };
175
+ };
176
+ } & {
177
+ "/:id": {
178
+ $patch: {
179
+ input: {
180
+ param: {
181
+ id: string;
182
+ };
183
+ };
184
+ output: {
185
+ error: string;
186
+ };
187
+ outputFormat: "json";
188
+ status: 404;
189
+ } | {
190
+ input: {
191
+ param: {
192
+ id: string;
193
+ };
194
+ };
195
+ output: {
196
+ data: {
197
+ active: boolean;
198
+ code: string | null;
199
+ metadata: import("hono/utils/types").JSONValue;
200
+ id: string;
201
+ description: string | null;
202
+ name: string;
203
+ slug: string;
204
+ discountType: "percentage" | "fixed_amount";
205
+ discountPercent: string | null;
206
+ discountAmountCents: number | null;
207
+ currency: string | null;
208
+ scope: {
209
+ kind: "global";
210
+ } | {
211
+ kind: "products";
212
+ productIds: string[];
213
+ } | {
214
+ kind: "categories";
215
+ categoryIds: string[];
216
+ } | {
217
+ kind: "destinations";
218
+ destinationIds: string[];
219
+ } | {
220
+ kind: "markets";
221
+ marketIds: string[];
222
+ } | {
223
+ kind: "audiences";
224
+ audiences: ("staff" | "customer" | "partner" | "supplier")[];
225
+ };
226
+ conditions: {
227
+ minPax?: number | undefined;
228
+ };
229
+ validFrom: string | null;
230
+ validUntil: string | null;
231
+ stackable: boolean;
232
+ createdAt: string;
233
+ updatedAt: string;
234
+ };
235
+ };
236
+ outputFormat: "json";
237
+ status: import("hono/utils/http-status").ContentfulStatusCode;
238
+ };
239
+ };
240
+ } & {
241
+ "/:id/archive": {
242
+ $post: {
243
+ input: {
244
+ param: {
245
+ id: string;
246
+ };
247
+ };
248
+ output: {
249
+ error: string;
250
+ };
251
+ outputFormat: "json";
252
+ status: 404;
253
+ } | {
254
+ input: {
255
+ param: {
256
+ id: string;
257
+ };
258
+ };
259
+ output: {
260
+ data: {
261
+ active: boolean;
262
+ code: string | null;
263
+ metadata: import("hono/utils/types").JSONValue;
264
+ id: string;
265
+ description: string | null;
266
+ name: string;
267
+ slug: string;
268
+ discountType: "percentage" | "fixed_amount";
269
+ discountPercent: string | null;
270
+ discountAmountCents: number | null;
271
+ currency: string | null;
272
+ scope: {
273
+ kind: "global";
274
+ } | {
275
+ kind: "products";
276
+ productIds: string[];
277
+ } | {
278
+ kind: "categories";
279
+ categoryIds: string[];
280
+ } | {
281
+ kind: "destinations";
282
+ destinationIds: string[];
283
+ } | {
284
+ kind: "markets";
285
+ marketIds: string[];
286
+ } | {
287
+ kind: "audiences";
288
+ audiences: ("staff" | "customer" | "partner" | "supplier")[];
289
+ };
290
+ conditions: {
291
+ minPax?: number | undefined;
292
+ };
293
+ validFrom: string | null;
294
+ validUntil: string | null;
295
+ stackable: boolean;
296
+ createdAt: string;
297
+ updatedAt: string;
298
+ };
299
+ };
300
+ outputFormat: "json";
301
+ status: import("hono/utils/http-status").ContentfulStatusCode;
302
+ };
303
+ };
304
+ } & {
305
+ "/:id": {
306
+ $delete: {
307
+ input: {
308
+ param: {
309
+ id: string;
310
+ };
311
+ };
312
+ output: {
313
+ error: string;
314
+ };
315
+ outputFormat: "json";
316
+ status: 404;
317
+ } | {
318
+ input: {
319
+ param: {
320
+ id: string;
321
+ };
322
+ };
323
+ output: {
324
+ data: {
325
+ id: string;
326
+ };
327
+ };
328
+ outputFormat: "json";
329
+ status: import("hono/utils/http-status").ContentfulStatusCode;
330
+ } | {
331
+ input: {
332
+ param: {
333
+ id: string;
334
+ };
335
+ };
336
+ output: {
337
+ error: string;
338
+ };
339
+ outputFormat: "json";
340
+ status: 409;
341
+ };
342
+ };
343
+ }, "/", "/:id">;
344
+ export type PromotionsRoutes = typeof promotionsRoutes;
345
+ //# sourceMappingURL=routes.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"routes.d.ts","sourceRoot":"","sources":["../src/routes.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAKH,OAAO,EAAE,KAAK,GAAG,EAAY,MAAM,oBAAoB,CAAA;AAQvD,eAAO,MAAM,gBAAgB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;eAgDzB,CAAA;AAEJ,MAAM,MAAM,gBAAgB,GAAG,OAAO,gBAAgB,CAAA"}
package/dist/routes.js ADDED
@@ -0,0 +1,55 @@
1
+ /**
2
+ * Admin routes for promotions — mounted by the operator template at
3
+ * `/v1/admin/promotions/*` (staff-actor-gated by the parent app's
4
+ * middleware chain).
5
+ *
6
+ * PR1 ships CRUD only. Public storefront routes are exposed via the
7
+ * existing `/v1/public/products/:productId/offers` endpoints in
8
+ * `@voyantjs/storefront` once the storefront resolver is wired in PR4.
9
+ */
10
+ import { parseJsonBody, parseQuery } from "@voyantjs/hono";
11
+ import { Hono } from "hono";
12
+ import { notFound } from "./routes-shared.js";
13
+ import { promotionsService } from "./service.js";
14
+ import { insertPromotionalOfferSchema, promotionalOfferListQuerySchema, updatePromotionalOfferSchema, } from "./validation.js";
15
+ export const promotionsRoutes = new Hono()
16
+ .get("/", async (c) => {
17
+ const query = await parseQuery(c, promotionalOfferListQuerySchema);
18
+ return c.json(await promotionsService.listOffers(c.get("db"), query));
19
+ })
20
+ .post("/", async (c) => c.json({
21
+ data: await promotionsService.createOffer(c.get("db"), await parseJsonBody(c, insertPromotionalOfferSchema), { eventBus: c.get("eventBus") }),
22
+ }, 201))
23
+ .get("/:id", async (c) => {
24
+ const offer = await promotionsService.getOfferById(c.get("db"), c.req.param("id"));
25
+ return offer ? c.json({ data: offer }) : notFound(c, "Promotional offer not found");
26
+ })
27
+ .patch("/:id", async (c) => {
28
+ const patch = await parseJsonBody(c, updatePromotionalOfferSchema);
29
+ const offer = await promotionsService.updateOffer(c.get("db"), c.req.param("id"), patch, {
30
+ eventBus: c.get("eventBus"),
31
+ });
32
+ return offer ? c.json({ data: offer }) : notFound(c, "Promotional offer not found");
33
+ })
34
+ .post("/:id/archive", async (c) => {
35
+ const offer = await promotionsService.archiveOffer(c.get("db"), c.req.param("id"), {
36
+ eventBus: c.get("eventBus"),
37
+ });
38
+ return offer ? c.json({ data: offer }) : notFound(c, "Promotional offer not found");
39
+ })
40
+ .delete("/:id", async (c) => {
41
+ try {
42
+ const result = await promotionsService.deleteOffer(c.get("db"), c.req.param("id"), {
43
+ eventBus: c.get("eventBus"),
44
+ });
45
+ return result ? c.json({ data: result }) : notFound(c, "Promotional offer not found");
46
+ }
47
+ catch (err) {
48
+ // `deleteOffer` throws when redemptions exist (the FK RESTRICT
49
+ // would otherwise surface a less helpful error). Translate to 409.
50
+ if (err instanceof Error && err.message.includes("redemption(s) exist")) {
51
+ return c.json({ error: err.message }, 409);
52
+ }
53
+ throw err;
54
+ }
55
+ });