@endora-commerce/mod-promotions 0.100.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/LICENSE +21 -0
- package/README.md +59 -0
- package/dist/admin/api/promotions-client.d.ts +98 -0
- package/dist/admin/api/promotions-client.d.ts.map +1 -0
- package/dist/admin/api/promotions-client.js +40 -0
- package/dist/admin/api/promotions-client.js.map +1 -0
- package/dist/admin/components/CouponGeneratorForm.d.ts +10 -0
- package/dist/admin/components/CouponGeneratorForm.d.ts.map +1 -0
- package/dist/admin/components/CouponGeneratorForm.js +56 -0
- package/dist/admin/components/CouponGeneratorForm.js.map +1 -0
- package/dist/admin/index.d.ts +43 -0
- package/dist/admin/index.d.ts.map +1 -0
- package/dist/admin/index.js +69 -0
- package/dist/admin/index.js.map +1 -0
- package/dist/admin/pages/PromotionEditPage.d.ts +9 -0
- package/dist/admin/pages/PromotionEditPage.d.ts.map +1 -0
- package/dist/admin/pages/PromotionEditPage.js +333 -0
- package/dist/admin/pages/PromotionEditPage.js.map +1 -0
- package/dist/admin/pages/PromotionRulesPage.d.ts +14 -0
- package/dist/admin/pages/PromotionRulesPage.d.ts.map +1 -0
- package/dist/admin/pages/PromotionRulesPage.js +86 -0
- package/dist/admin/pages/PromotionRulesPage.js.map +1 -0
- package/dist/admin/pages/PromotionStatsPage.d.ts +13 -0
- package/dist/admin/pages/PromotionStatsPage.d.ts.map +1 -0
- package/dist/admin/pages/PromotionStatsPage.js +44 -0
- package/dist/admin/pages/PromotionStatsPage.js.map +1 -0
- package/dist/admin/pages/PromotionsPage.d.ts +9 -0
- package/dist/admin/pages/PromotionsPage.d.ts.map +1 -0
- package/dist/admin/pages/PromotionsPage.js +79 -0
- package/dist/admin/pages/PromotionsPage.js.map +1 -0
- package/dist/backend/actions/amount-off-cart.d.ts +4 -0
- package/dist/backend/actions/amount-off-cart.d.ts.map +1 -0
- package/dist/backend/actions/amount-off-cart.js +16 -0
- package/dist/backend/actions/amount-off-cart.js.map +1 -0
- package/dist/backend/actions/buy-x-get-y-free.d.ts +8 -0
- package/dist/backend/actions/buy-x-get-y-free.d.ts.map +1 -0
- package/dist/backend/actions/buy-x-get-y-free.js +20 -0
- package/dist/backend/actions/buy-x-get-y-free.js.map +1 -0
- package/dist/backend/actions/buy-x-units-amount-off.d.ts +4 -0
- package/dist/backend/actions/buy-x-units-amount-off.d.ts.map +1 -0
- package/dist/backend/actions/buy-x-units-amount-off.js +19 -0
- package/dist/backend/actions/buy-x-units-amount-off.js.map +1 -0
- package/dist/backend/actions/buy-x-units-percent-off.d.ts +4 -0
- package/dist/backend/actions/buy-x-units-percent-off.d.ts.map +1 -0
- package/dist/backend/actions/buy-x-units-percent-off.js +15 -0
- package/dist/backend/actions/buy-x-units-percent-off.js.map +1 -0
- package/dist/backend/actions/buy-x-units-y-free.d.ts +7 -0
- package/dist/backend/actions/buy-x-units-y-free.d.ts.map +1 -0
- package/dist/backend/actions/buy-x-units-y-free.js +21 -0
- package/dist/backend/actions/buy-x-units-y-free.js.map +1 -0
- package/dist/backend/actions/every-nth-product-percent-off.d.ts +7 -0
- package/dist/backend/actions/every-nth-product-percent-off.d.ts.map +1 -0
- package/dist/backend/actions/every-nth-product-percent-off.js +17 -0
- package/dist/backend/actions/every-nth-product-percent-off.js.map +1 -0
- package/dist/backend/actions/free-delivery.d.ts +4 -0
- package/dist/backend/actions/free-delivery.d.ts.map +1 -0
- package/dist/backend/actions/free-delivery.js +11 -0
- package/dist/backend/actions/free-delivery.js.map +1 -0
- package/dist/backend/actions/percentage-off-cart.d.ts +4 -0
- package/dist/backend/actions/percentage-off-cart.d.ts.map +1 -0
- package/dist/backend/actions/percentage-off-cart.js +11 -0
- package/dist/backend/actions/percentage-off-cart.js.map +1 -0
- package/dist/backend/actions/spend-x-amount-off.d.ts +4 -0
- package/dist/backend/actions/spend-x-amount-off.d.ts.map +1 -0
- package/dist/backend/actions/spend-x-amount-off.js +17 -0
- package/dist/backend/actions/spend-x-amount-off.js.map +1 -0
- package/dist/backend/actions/spend-x-percent-off.d.ts +4 -0
- package/dist/backend/actions/spend-x-percent-off.d.ts.map +1 -0
- package/dist/backend/actions/spend-x-percent-off.js +18 -0
- package/dist/backend/actions/spend-x-percent-off.js.map +1 -0
- package/dist/backend/actions/types.d.ts +37 -0
- package/dist/backend/actions/types.d.ts.map +1 -0
- package/dist/backend/actions/types.js +2 -0
- package/dist/backend/actions/types.js.map +1 -0
- package/dist/backend/actions/util.d.ts +13 -0
- package/dist/backend/actions/util.d.ts.map +1 -0
- package/dist/backend/actions/util.js +18 -0
- package/dist/backend/actions/util.js.map +1 -0
- package/dist/backend/entities/coupon-batch.entity.d.ts +21 -0
- package/dist/backend/entities/coupon-batch.entity.d.ts.map +1 -0
- package/dist/backend/entities/coupon-batch.entity.js +78 -0
- package/dist/backend/entities/coupon-batch.entity.js.map +1 -0
- package/dist/backend/entities/promotion-coupon.entity.d.ts +17 -0
- package/dist/backend/entities/promotion-coupon.entity.d.ts.map +1 -0
- package/dist/backend/entities/promotion-coupon.entity.js +64 -0
- package/dist/backend/entities/promotion-coupon.entity.js.map +1 -0
- package/dist/backend/entities/promotion-rule.entity.d.ts +16 -0
- package/dist/backend/entities/promotion-rule.entity.d.ts.map +1 -0
- package/dist/backend/entities/promotion-rule.entity.js +57 -0
- package/dist/backend/entities/promotion-rule.entity.js.map +1 -0
- package/dist/backend/entities/promotion-usage-counter.entity.d.ts +15 -0
- package/dist/backend/entities/promotion-usage-counter.entity.d.ts.map +1 -0
- package/dist/backend/entities/promotion-usage-counter.entity.js +48 -0
- package/dist/backend/entities/promotion-usage-counter.entity.js.map +1 -0
- package/dist/backend/entities/promotion-usage.entity.d.ts +21 -0
- package/dist/backend/entities/promotion-usage.entity.d.ts.map +1 -0
- package/dist/backend/entities/promotion-usage.entity.js +89 -0
- package/dist/backend/entities/promotion-usage.entity.js.map +1 -0
- package/dist/backend/entities/promotion.entity.d.ts +63 -0
- package/dist/backend/entities/promotion.entity.d.ts.map +1 -0
- package/dist/backend/entities/promotion.entity.js +194 -0
- package/dist/backend/entities/promotion.entity.js.map +1 -0
- package/dist/backend/index.d.ts +124 -0
- package/dist/backend/index.d.ts.map +1 -0
- package/dist/backend/index.js +182 -0
- package/dist/backend/index.js.map +1 -0
- package/dist/backend/routes.d.ts +88 -0
- package/dist/backend/routes.d.ts.map +1 -0
- package/dist/backend/routes.js +256 -0
- package/dist/backend/routes.js.map +1 -0
- package/dist/backend/services/coupon-service.d.ts +40 -0
- package/dist/backend/services/coupon-service.d.ts.map +1 -0
- package/dist/backend/services/coupon-service.js +178 -0
- package/dist/backend/services/coupon-service.js.map +1 -0
- package/dist/backend/services/promotion-action-registry.d.ts +20 -0
- package/dist/backend/services/promotion-action-registry.d.ts.map +1 -0
- package/dist/backend/services/promotion-action-registry.js +57 -0
- package/dist/backend/services/promotion-action-registry.js.map +1 -0
- package/dist/backend/services/promotion-code-port.d.ts +24 -0
- package/dist/backend/services/promotion-code-port.d.ts.map +1 -0
- package/dist/backend/services/promotion-code-port.js +53 -0
- package/dist/backend/services/promotion-code-port.js.map +1 -0
- package/dist/backend/services/promotion-currency-reference.d.ts +13 -0
- package/dist/backend/services/promotion-currency-reference.d.ts.map +1 -0
- package/dist/backend/services/promotion-currency-reference.js +25 -0
- package/dist/backend/services/promotion-currency-reference.js.map +1 -0
- package/dist/backend/services/promotion-rule-evaluator.d.ts +32 -0
- package/dist/backend/services/promotion-rule-evaluator.d.ts.map +1 -0
- package/dist/backend/services/promotion-rule-evaluator.js +131 -0
- package/dist/backend/services/promotion-rule-evaluator.js.map +1 -0
- package/dist/backend/services/promotion-rule-service.d.ts +62 -0
- package/dist/backend/services/promotion-rule-service.d.ts.map +1 -0
- package/dist/backend/services/promotion-rule-service.js +95 -0
- package/dist/backend/services/promotion-rule-service.js.map +1 -0
- package/dist/backend/services/promotion-rule-store.d.ts +33 -0
- package/dist/backend/services/promotion-rule-store.d.ts.map +1 -0
- package/dist/backend/services/promotion-rule-store.js +88 -0
- package/dist/backend/services/promotion-rule-store.js.map +1 -0
- package/dist/backend/services/promotion-service.d.ts +312 -0
- package/dist/backend/services/promotion-service.d.ts.map +1 -0
- package/dist/backend/services/promotion-service.js +762 -0
- package/dist/backend/services/promotion-service.js.map +1 -0
- package/dist/backend/services/promotion-stats-service.d.ts +15 -0
- package/dist/backend/services/promotion-stats-service.d.ts.map +1 -0
- package/dist/backend/services/promotion-stats-service.js +66 -0
- package/dist/backend/services/promotion-stats-service.js.map +1 -0
- package/dist/backend/services/promotion-usage-service.d.ts +46 -0
- package/dist/backend/services/promotion-usage-service.d.ts.map +1 -0
- package/dist/backend/services/promotion-usage-service.js +168 -0
- package/dist/backend/services/promotion-usage-service.js.map +1 -0
- package/dist/manifest.d.ts +172 -0
- package/dist/manifest.d.ts.map +1 -0
- package/dist/manifest.js +140 -0
- package/dist/manifest.js.map +1 -0
- package/dist/migrations/20260505T074605_promotions_criteria.d.ts +19 -0
- package/dist/migrations/20260505T074605_promotions_criteria.d.ts.map +1 -0
- package/dist/migrations/20260505T074605_promotions_criteria.js +23 -0
- package/dist/migrations/20260505T074605_promotions_criteria.js.map +1 -0
- package/dist/migrations/20260618T100727_promotions_engine.d.ts +19 -0
- package/dist/migrations/20260618T100727_promotions_engine.d.ts.map +1 -0
- package/dist/migrations/20260618T100727_promotions_engine.js +148 -0
- package/dist/migrations/20260618T100727_promotions_engine.js.map +1 -0
- package/dist/migrations/20260818T081251_promotions_promotion_usage_order_fk.d.ts +42 -0
- package/dist/migrations/20260818T081251_promotions_promotion_usage_order_fk.d.ts.map +1 -0
- package/dist/migrations/20260818T081251_promotions_promotion_usage_order_fk.js +76 -0
- package/dist/migrations/20260818T081251_promotions_promotion_usage_order_fk.js.map +1 -0
- package/dist/migrations/20260912T094715_promotions_sales_channel_promotions.d.ts +27 -0
- package/dist/migrations/20260912T094715_promotions_sales_channel_promotions.d.ts.map +1 -0
- package/dist/migrations/20260912T094715_promotions_sales_channel_promotions.js +44 -0
- package/dist/migrations/20260912T094715_promotions_sales_channel_promotions.js.map +1 -0
- package/dist/migrations/index.d.ts +39 -0
- package/dist/migrations/index.d.ts.map +1 -0
- package/dist/migrations/index.js +44 -0
- package/dist/migrations/index.js.map +1 -0
- package/dist/ports/index.d.ts +106 -0
- package/dist/ports/index.d.ts.map +1 -0
- package/dist/ports/index.js +2 -0
- package/dist/ports/index.js.map +1 -0
- package/docs/promotions.md +96 -0
- package/i18n/en.json +34 -0
- package/i18n/pl.json +34 -0
- package/package.json +101 -0
- package/tailwind.css +14 -0
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
import { bulkCouponActiveRequestSchema, cartSnapshotSchema, generateCouponsRequestSchema, upsertPromotionRequestSchema, upsertPromotionRuleRequestSchema, } from '@endora-commerce/contracts';
|
|
2
|
+
import { promotionStatsGroupBySchema } from '@endora-commerce/contracts';
|
|
3
|
+
import { PROMOTION_PERMISSIONS } from '../manifest.js';
|
|
4
|
+
export async function registerPromotionRoutes(app, deps) {
|
|
5
|
+
const { promotionService, couponService, ruleStore, statsService, requireAdmin, catalogPromoAttributes, ruleTargets, } = deps;
|
|
6
|
+
const parseStatsQuery = (q) => {
|
|
7
|
+
const out = {};
|
|
8
|
+
if (typeof q['from'] === 'string')
|
|
9
|
+
out.from = q['from'];
|
|
10
|
+
if (typeof q['to'] === 'string')
|
|
11
|
+
out.to = q['to'];
|
|
12
|
+
const gb = promotionStatsGroupBySchema.safeParse(q['groupBy']);
|
|
13
|
+
if (gb.success)
|
|
14
|
+
out.groupBy = gb.data;
|
|
15
|
+
return out;
|
|
16
|
+
};
|
|
17
|
+
const readGate = requireAdmin(PROMOTION_PERMISSIONS.READ);
|
|
18
|
+
const writeGate = requireAdmin(PROMOTION_PERMISSIONS.WRITE);
|
|
19
|
+
const deleteGate = requireAdmin(PROMOTION_PERMISSIONS.DELETE);
|
|
20
|
+
app.get('/api/v1/admin/promotions', { preHandler: readGate }, async () => {
|
|
21
|
+
const rows = await promotionService.list();
|
|
22
|
+
return { data: rows.map(serialize) };
|
|
23
|
+
});
|
|
24
|
+
app.get('/api/v1/admin/promotions/:id', { preHandler: readGate }, async (request) => {
|
|
25
|
+
const row = await promotionService.getById(request.params.id);
|
|
26
|
+
return { data: serialize(row) };
|
|
27
|
+
});
|
|
28
|
+
// Feature 045 — action catalogue introspection for the admin action picker.
|
|
29
|
+
app.get('/api/v1/admin/promotions/action-types', { preHandler: readGate }, async () => {
|
|
30
|
+
return { data: { items: promotionService.listActionTypes() } };
|
|
31
|
+
});
|
|
32
|
+
// Feature 045 (US6) — standalone named rules.
|
|
33
|
+
app.get('/api/v1/admin/promotion-rules', { preHandler: readGate }, async () => {
|
|
34
|
+
const rows = await ruleStore.list();
|
|
35
|
+
return { data: rows.map(serializeRule) };
|
|
36
|
+
});
|
|
37
|
+
app.get('/api/v1/admin/promotion-rules/:id', { preHandler: readGate }, async (request) => {
|
|
38
|
+
const row = await ruleStore.getById(request.params.id);
|
|
39
|
+
const usedBy = await ruleStore.usedBy(request.params.id);
|
|
40
|
+
return { data: serializeRule(row), usedBy };
|
|
41
|
+
});
|
|
42
|
+
app.post('/api/v1/admin/promotion-rules', { preHandler: writeGate, schema: { body: upsertPromotionRuleRequestSchema } }, async (request, reply) => {
|
|
43
|
+
const body = upsertPromotionRuleRequestSchema.parse(request.body);
|
|
44
|
+
const row = await ruleStore.create({
|
|
45
|
+
name: body.name,
|
|
46
|
+
...(body.description !== undefined ? { description: body.description } : {}),
|
|
47
|
+
definition: body.definition,
|
|
48
|
+
});
|
|
49
|
+
reply.status(201);
|
|
50
|
+
return { data: serializeRule(row) };
|
|
51
|
+
});
|
|
52
|
+
app.put('/api/v1/admin/promotion-rules/:id', { preHandler: writeGate, schema: { body: upsertPromotionRuleRequestSchema } }, async (request) => {
|
|
53
|
+
const body = upsertPromotionRuleRequestSchema.parse(request.body);
|
|
54
|
+
const row = await ruleStore.update(request.params.id, {
|
|
55
|
+
name: body.name,
|
|
56
|
+
...(body.description !== undefined ? { description: body.description } : {}),
|
|
57
|
+
definition: body.definition,
|
|
58
|
+
});
|
|
59
|
+
return { data: serializeRule(row) };
|
|
60
|
+
});
|
|
61
|
+
app.delete('/api/v1/admin/promotion-rules/:id', { preHandler: deleteGate }, async (request, reply) => {
|
|
62
|
+
await ruleStore.remove(request.params.id);
|
|
63
|
+
return reply.status(204).send();
|
|
64
|
+
});
|
|
65
|
+
app.post('/api/v1/admin/promotions', { preHandler: writeGate, schema: { body: upsertPromotionRequestSchema } }, async (request, reply) => {
|
|
66
|
+
const body = upsertPromotionRequestSchema.parse(request.body);
|
|
67
|
+
const row = await promotionService.upsert(toUpsertInput(body));
|
|
68
|
+
reply.status(201);
|
|
69
|
+
return { data: serialize(row) };
|
|
70
|
+
});
|
|
71
|
+
app.put('/api/v1/admin/promotions/:id', { preHandler: writeGate, schema: { body: upsertPromotionRequestSchema } }, async (request) => {
|
|
72
|
+
const body = upsertPromotionRequestSchema.parse(request.body);
|
|
73
|
+
const row = await promotionService.updateById(request.params.id, toUpsertInput(body));
|
|
74
|
+
return { data: serialize(row) };
|
|
75
|
+
});
|
|
76
|
+
// Feature 012 / US8 — picker payload for the Promotion Rule editor.
|
|
77
|
+
app.get('/api/v1/admin/promotions/rule-targets/attributes', { preHandler: readGate }, async () => {
|
|
78
|
+
const keys = await catalogPromoAttributes.promoRuleAttributeKeys();
|
|
79
|
+
const items = [];
|
|
80
|
+
for (const k of keys) {
|
|
81
|
+
const meta = await catalogPromoAttributes.getAttributeWithOptions(k);
|
|
82
|
+
if (!meta)
|
|
83
|
+
continue;
|
|
84
|
+
const isSelectStyle = meta.valueType === 'select' ||
|
|
85
|
+
meta.valueType === 'enum' ||
|
|
86
|
+
meta.valueType === 'multiselect';
|
|
87
|
+
items.push({
|
|
88
|
+
id: meta.id,
|
|
89
|
+
key: meta.key,
|
|
90
|
+
label: meta.label,
|
|
91
|
+
labelDefault: meta.labelDefault,
|
|
92
|
+
valueType: meta.valueType,
|
|
93
|
+
...(isSelectStyle ? { options: meta.options } : {}),
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
return { data: { items } };
|
|
97
|
+
});
|
|
98
|
+
// Feature 045 (US3) — single coupon management for a promotion.
|
|
99
|
+
app.get('/api/v1/admin/promotions/:id/coupons', { preHandler: readGate }, async (request) => {
|
|
100
|
+
const coupons = await couponService.listForPromotion(request.params.id);
|
|
101
|
+
return { data: coupons.map(serializeCoupon) };
|
|
102
|
+
});
|
|
103
|
+
app.post('/api/v1/admin/promotions/:id/coupons', { preHandler: writeGate }, async (request, reply) => {
|
|
104
|
+
const code = typeof request.body?.code === 'string' ? request.body.code.trim() : '';
|
|
105
|
+
if (!code) {
|
|
106
|
+
reply.status(400);
|
|
107
|
+
return { error: { code: 'validation_failed', message: 'code is required' } };
|
|
108
|
+
}
|
|
109
|
+
const coupon = await couponService.createSingle(request.params.id, code);
|
|
110
|
+
reply.status(201);
|
|
111
|
+
return { data: serializeCoupon(coupon) };
|
|
112
|
+
});
|
|
113
|
+
// Bulk activate/deactivate selected coupons of a promotion.
|
|
114
|
+
app.post('/api/v1/admin/promotions/:id/coupons/bulk-active', { preHandler: writeGate, schema: { body: bulkCouponActiveRequestSchema } }, async (request) => {
|
|
115
|
+
const body = bulkCouponActiveRequestSchema.parse(request.body);
|
|
116
|
+
const updated = await couponService.setActiveBulk(request.params.id, body.couponIds, body.isActive);
|
|
117
|
+
return { data: { updated } };
|
|
118
|
+
});
|
|
119
|
+
// Feature 045 (US4) — bulk coupon generator.
|
|
120
|
+
app.post('/api/v1/admin/promotions/:id/coupon-batches', { preHandler: writeGate, schema: { body: generateCouponsRequestSchema } }, async (request, reply) => {
|
|
121
|
+
const body = generateCouponsRequestSchema.parse(request.body);
|
|
122
|
+
const result = await couponService.generateBatch(request.params.id, body);
|
|
123
|
+
reply.status(201);
|
|
124
|
+
return { data: { batch: { id: result.batch.id }, generated: result.generated } };
|
|
125
|
+
});
|
|
126
|
+
app.get('/api/v1/admin/promotions/:id/coupon-batches/:batchId/export', { preHandler: readGate, config: { streamingResponse: true } }, async (request, reply) => {
|
|
127
|
+
const codes = await couponService.listBatchCodes(request.params.batchId);
|
|
128
|
+
reply.header('content-type', 'text/csv; charset=utf-8');
|
|
129
|
+
reply.header('content-disposition', `attachment; filename="coupons-${request.params.batchId}.csv"`);
|
|
130
|
+
return reply.send(`code\n${codes.join('\n')}\n`);
|
|
131
|
+
});
|
|
132
|
+
// Feature 045 (T033) — rule-target pickers feeding the Rule Builder.
|
|
133
|
+
app.get('/api/v1/admin/promotions/rule-targets/sales-channels', { preHandler: readGate }, async () => ({
|
|
134
|
+
data: { items: (await ruleTargets.salesChannels?.()) ?? [] },
|
|
135
|
+
}));
|
|
136
|
+
app.get('/api/v1/admin/promotions/rule-targets/customer-groups', { preHandler: readGate }, async () => ({
|
|
137
|
+
data: { items: (await ruleTargets.customerGroups?.()) ?? [] },
|
|
138
|
+
}));
|
|
139
|
+
app.get('/api/v1/admin/promotions/rule-targets/organizations', { preHandler: readGate }, async () => ({
|
|
140
|
+
data: { items: (await ruleTargets.organizations?.()) ?? [] },
|
|
141
|
+
}));
|
|
142
|
+
app.get('/api/v1/admin/promotions/rule-targets/categories', { preHandler: readGate }, async () => ({
|
|
143
|
+
data: { items: (await ruleTargets.categories?.()) ?? [] },
|
|
144
|
+
}));
|
|
145
|
+
app.get('/api/v1/admin/promotions/rule-targets/payment-methods', { preHandler: readGate }, async () => ({
|
|
146
|
+
data: { items: (await ruleTargets.paymentMethods?.()) ?? [] },
|
|
147
|
+
}));
|
|
148
|
+
app.get('/api/v1/admin/promotions/rule-targets/delivery-methods', { preHandler: readGate }, async () => ({
|
|
149
|
+
data: { items: (await ruleTargets.deliveryMethods?.()) ?? [] },
|
|
150
|
+
}));
|
|
151
|
+
app.delete('/api/v1/admin/promotions/:id', { preHandler: deleteGate }, async (request, reply) => {
|
|
152
|
+
await promotionService.remove(request.params.id);
|
|
153
|
+
return reply.status(204).send();
|
|
154
|
+
});
|
|
155
|
+
// Feature 045 (US7) — usage statistics.
|
|
156
|
+
app.get('/api/v1/admin/promotions/:id/stats', { preHandler: readGate }, async (request) => {
|
|
157
|
+
const data = await statsService.forPromotion(request.params.id, parseStatsQuery(request.query ?? {}));
|
|
158
|
+
return { data };
|
|
159
|
+
});
|
|
160
|
+
app.get('/api/v1/admin/promotions/coupons/:couponId/stats', { preHandler: readGate }, async (request) => {
|
|
161
|
+
const data = await statsService.forCoupon(request.params.couponId, parseStatsQuery(request.query ?? {}));
|
|
162
|
+
return { data };
|
|
163
|
+
});
|
|
164
|
+
app.post('/api/v1/admin/promotions/preview', { preHandler: readGate, schema: { body: cartSnapshotSchema } }, async (request) => {
|
|
165
|
+
const snapshot = cartSnapshotSchema.parse(request.body);
|
|
166
|
+
const result = await promotionService.applyToCart(snapshot);
|
|
167
|
+
return { data: result };
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
/** Pass through validated request fields into the service input. */
|
|
171
|
+
function toUpsertInput(body) {
|
|
172
|
+
return {
|
|
173
|
+
name: body.name,
|
|
174
|
+
...(body.code !== undefined ? { code: body.code } : {}),
|
|
175
|
+
...(body.kind !== undefined ? { kind: body.kind } : {}),
|
|
176
|
+
...(body.value !== undefined ? { value: body.value } : {}),
|
|
177
|
+
...(body.currency !== undefined ? { currency: body.currency } : {}),
|
|
178
|
+
...(body.minCartSubtotal !== undefined ? { minCartSubtotal: body.minCartSubtotal } : {}),
|
|
179
|
+
...(body.validFrom !== undefined ? { validFrom: body.validFrom } : {}),
|
|
180
|
+
...(body.validUntil !== undefined ? { validUntil: body.validUntil } : {}),
|
|
181
|
+
...(body.organizationId !== undefined ? { organizationId: body.organizationId } : {}),
|
|
182
|
+
...(body.customerGroupId !== undefined ? { customerGroupId: body.customerGroupId } : {}),
|
|
183
|
+
...(body.categoryId !== undefined ? { categoryId: body.categoryId } : {}),
|
|
184
|
+
...(body.productId !== undefined ? { productId: body.productId } : {}),
|
|
185
|
+
...(body.criteria !== undefined ? { criteria: body.criteria } : {}),
|
|
186
|
+
...(body.isActive !== undefined ? { isActive: body.isActive } : {}),
|
|
187
|
+
...(body.description !== undefined ? { description: body.description } : {}),
|
|
188
|
+
...(body.priority !== undefined ? { priority: body.priority } : {}),
|
|
189
|
+
...(body.stopFurther !== undefined ? { stopFurther: body.stopFurther } : {}),
|
|
190
|
+
...(body.action !== undefined ? { action: body.action } : {}),
|
|
191
|
+
...(body.ruleId !== undefined ? { ruleId: body.ruleId } : {}),
|
|
192
|
+
...(body.rule !== undefined ? { rule: body.rule } : {}),
|
|
193
|
+
...(body.usageLimitGlobal !== undefined ? { usageLimitGlobal: body.usageLimitGlobal } : {}),
|
|
194
|
+
...(body.usageLimitPerOrganization !== undefined
|
|
195
|
+
? { usageLimitPerOrganization: body.usageLimitPerOrganization }
|
|
196
|
+
: {}),
|
|
197
|
+
...(body.usageLimitPerCustomer !== undefined
|
|
198
|
+
? { usageLimitPerCustomer: body.usageLimitPerCustomer }
|
|
199
|
+
: {}),
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
function serializeRule(r) {
|
|
203
|
+
return {
|
|
204
|
+
id: r.id,
|
|
205
|
+
name: r.name,
|
|
206
|
+
description: r.description ?? null,
|
|
207
|
+
definition: r.definition,
|
|
208
|
+
createdAt: r.createdAt.toISOString(),
|
|
209
|
+
updatedAt: r.updatedAt.toISOString(),
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
function serializeCoupon(c) {
|
|
213
|
+
return {
|
|
214
|
+
id: c.id,
|
|
215
|
+
promotionId: c.promotionId,
|
|
216
|
+
batchId: c.batchId ?? null,
|
|
217
|
+
code: c.code,
|
|
218
|
+
limitScope: c.limitScope,
|
|
219
|
+
isActive: c.isActive,
|
|
220
|
+
createdAt: c.createdAt.toISOString(),
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
function serialize(row) {
|
|
224
|
+
const action = row.actionType
|
|
225
|
+
? { type: row.actionType, ...row.actionConfig }
|
|
226
|
+
: null;
|
|
227
|
+
return {
|
|
228
|
+
id: row.id,
|
|
229
|
+
code: row.code ?? null,
|
|
230
|
+
name: row.name,
|
|
231
|
+
kind: row.kind ?? null,
|
|
232
|
+
value: row.value != null ? Number(row.value) : null,
|
|
233
|
+
currency: row.currency ?? null,
|
|
234
|
+
minCartSubtotal: row.minCartSubtotal != null ? Number(row.minCartSubtotal) : null,
|
|
235
|
+
validFrom: row.validFrom?.toISOString() ?? null,
|
|
236
|
+
validUntil: row.validUntil?.toISOString() ?? null,
|
|
237
|
+
organizationId: row.organizationId ?? null,
|
|
238
|
+
customerGroupId: row.customerGroupId ?? null,
|
|
239
|
+
categoryId: row.categoryId ?? null,
|
|
240
|
+
productId: row.productId ?? null,
|
|
241
|
+
criteria: row.criteria ?? [],
|
|
242
|
+
isActive: row.isActive,
|
|
243
|
+
description: row.description ?? null,
|
|
244
|
+
priority: row.priority,
|
|
245
|
+
stopFurther: row.stopFurther,
|
|
246
|
+
action,
|
|
247
|
+
ruleId: row.ruleId ?? null,
|
|
248
|
+
rule: row.ruleDefinition ?? null,
|
|
249
|
+
usageLimitGlobal: row.usageLimitGlobal ?? null,
|
|
250
|
+
usageLimitPerOrganization: row.usageLimitPerOrganization ?? null,
|
|
251
|
+
usageLimitPerCustomer: row.usageLimitPerCustomer ?? null,
|
|
252
|
+
createdAt: row.createdAt.toISOString(),
|
|
253
|
+
updatedAt: row.updatedAt.toISOString(),
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
//# sourceMappingURL=routes.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"routes.js","sourceRoot":"","sources":["../../src/backend/routes.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { EntityManager } from '@mikro-orm/postgresql';
|
|
2
|
+
import { type GenerateCouponsRequest } from '@endora-commerce/contracts';
|
|
3
|
+
import { PromotionCoupon } from '../entities/promotion-coupon.entity.js';
|
|
4
|
+
import { CouponBatch } from '../entities/coupon-batch.entity.js';
|
|
5
|
+
import type { AuditPort } from '@endora-commerce/platform/kernel';
|
|
6
|
+
/** Hard cap on a single generated batch (avoids a speculative queue, Principle X). */
|
|
7
|
+
export declare const COUPON_BATCH_MAX = 50000;
|
|
8
|
+
export declare function buildCouponCode(req: GenerateCouponsRequest): string;
|
|
9
|
+
/**
|
|
10
|
+
* Feature 045 (US3/US4) — coupon management. Single specified coupons land
|
|
11
|
+
* here with a globally-unique `code`; the generator (US4) bulk-inserts into
|
|
12
|
+
* the same table. The engine and the cart coupon flow resolve a presented
|
|
13
|
+
* code to its promotion through `findByCode`.
|
|
14
|
+
*/
|
|
15
|
+
export declare class CouponService {
|
|
16
|
+
#private;
|
|
17
|
+
private readonly emFactory;
|
|
18
|
+
private readonly auditLog?;
|
|
19
|
+
constructor(emFactory: () => EntityManager, auditLog?: AuditPort | undefined);
|
|
20
|
+
listForPromotion(promotionId: string): Promise<PromotionCoupon[]>;
|
|
21
|
+
findByCode(code: string): Promise<PromotionCoupon | null>;
|
|
22
|
+
/**
|
|
23
|
+
* Bulk activate/deactivate coupons by id, scoped to a promotion so an id from
|
|
24
|
+
* another promotion can never be flipped. Returns the number of rows updated.
|
|
25
|
+
*/
|
|
26
|
+
setActiveBulk(promotionId: string, couponIds: string[], isActive: boolean): Promise<number>;
|
|
27
|
+
/** Create a single specified coupon for a promotion. Rejects duplicates. */
|
|
28
|
+
createSingle(promotionId: string, code: string): Promise<PromotionCoupon>;
|
|
29
|
+
/**
|
|
30
|
+
* Generate a batch of unique coupon codes (US4). Uniqueness is guaranteed
|
|
31
|
+
* in-batch via a Set and cross-batch via a DB existence check + regenerate;
|
|
32
|
+
* the `code` UNIQUE constraint is the final backstop.
|
|
33
|
+
*/
|
|
34
|
+
generateBatch(promotionId: string, req: GenerateCouponsRequest): Promise<{
|
|
35
|
+
batch: CouponBatch;
|
|
36
|
+
generated: number;
|
|
37
|
+
}>;
|
|
38
|
+
listBatchCodes(batchId: string): Promise<string[]>;
|
|
39
|
+
}
|
|
40
|
+
//# sourceMappingURL=coupon-service.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"coupon-service.d.ts","sourceRoot":"","sources":["../../../src/backend/services/coupon-service.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;AAC3D,OAAO,EAAe,KAAK,sBAAsB,EAAE,MAAM,4BAA4B,CAAC;AAGtF,OAAO,EAAE,eAAe,EAAE,MAAM,wCAAwC,CAAC;AACzE,OAAO,EAAE,WAAW,EAAE,MAAM,oCAAoC,CAAC;AAEjE,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,kCAAkC,CAAC;AAElE,sFAAsF;AACtF,eAAO,MAAM,gBAAgB,QAAS,CAAC;AAsBvC,wBAAgB,eAAe,CAAC,GAAG,EAAE,sBAAsB,GAAG,MAAM,CAGnE;AAED;;;;;GAKG;AACH,qBAAa,aAAa;;IAEtB,OAAO,CAAC,QAAQ,CAAC,SAAS;IAC1B,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAC;gBADT,SAAS,EAAE,MAAM,aAAa,EAC9B,QAAQ,CAAC,EAAE,SAAS,YAAA;IAoBjC,gBAAgB,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,eAAe,EAAE,CAAC;IAQjE,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,eAAe,GAAG,IAAI,CAAC;IAI/D;;;OAGG;IACG,aAAa,CACjB,WAAW,EAAE,MAAM,EACnB,SAAS,EAAE,MAAM,EAAE,EACnB,QAAQ,EAAE,OAAO,GAChB,OAAO,CAAC,MAAM,CAAC;IAalB,4EAA4E;IACtE,YAAY,CAAC,WAAW,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,eAAe,CAAC;IAuB/E;;;;OAIG;IACG,aAAa,CACjB,WAAW,EAAE,MAAM,EACnB,GAAG,EAAE,sBAAsB,GAC1B,OAAO,CAAC;QAAE,KAAK,EAAE,WAAW,CAAC;QAAC,SAAS,EAAE,MAAM,CAAA;KAAE,CAAC;IA4E/C,cAAc,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC;CAQzD"}
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import { randomInt } from 'node:crypto';
|
|
2
|
+
import { ERROR_CODES } from '@endora-commerce/contracts';
|
|
3
|
+
import { HttpError } from '@endora-commerce/platform/http';
|
|
4
|
+
import { Promotion } from '../entities/promotion.entity.js';
|
|
5
|
+
import { PromotionCoupon } from '../entities/promotion-coupon.entity.js';
|
|
6
|
+
import { CouponBatch } from '../entities/coupon-batch.entity.js';
|
|
7
|
+
import { recordAuditFromContext } from '@endora-commerce/platform/commands';
|
|
8
|
+
/** Hard cap on a single generated batch (avoids a speculative queue, Principle X). */
|
|
9
|
+
export const COUPON_BATCH_MAX = 50_000;
|
|
10
|
+
const ALPHABETS = {
|
|
11
|
+
// Crockford-ish alphabet — no ambiguous I/O/0/1.
|
|
12
|
+
alnum: 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789',
|
|
13
|
+
digits: '0123456789',
|
|
14
|
+
letters: 'ABCDEFGHJKLMNPQRSTUVWXYZ',
|
|
15
|
+
};
|
|
16
|
+
function randomBody(length, alphabet) {
|
|
17
|
+
let out = '';
|
|
18
|
+
for (let i = 0; i < length; i += 1)
|
|
19
|
+
out += alphabet[randomInt(alphabet.length)];
|
|
20
|
+
return out;
|
|
21
|
+
}
|
|
22
|
+
function insertDashes(body, every) {
|
|
23
|
+
if (!every || every <= 0)
|
|
24
|
+
return body;
|
|
25
|
+
const parts = [];
|
|
26
|
+
for (let i = 0; i < body.length; i += every)
|
|
27
|
+
parts.push(body.slice(i, i + every));
|
|
28
|
+
return parts.join('-');
|
|
29
|
+
}
|
|
30
|
+
export function buildCouponCode(req) {
|
|
31
|
+
const body = insertDashes(randomBody(req.length, ALPHABETS[req.format]), req.dashEvery ?? 0);
|
|
32
|
+
return `${req.prefix ?? ''}${body}${req.suffix ?? ''}`;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Feature 045 (US3/US4) — coupon management. Single specified coupons land
|
|
36
|
+
* here with a globally-unique `code`; the generator (US4) bulk-inserts into
|
|
37
|
+
* the same table. The engine and the cart coupon flow resolve a presented
|
|
38
|
+
* code to its promotion through `findByCode`.
|
|
39
|
+
*/
|
|
40
|
+
export class CouponService {
|
|
41
|
+
emFactory;
|
|
42
|
+
auditLog;
|
|
43
|
+
constructor(emFactory, auditLog) {
|
|
44
|
+
this.emFactory = emFactory;
|
|
45
|
+
this.auditLog = auditLog;
|
|
46
|
+
}
|
|
47
|
+
#audit(em, action, objectId, stateAfter) {
|
|
48
|
+
if (this.auditLog) {
|
|
49
|
+
recordAuditFromContext(this.auditLog, em, {
|
|
50
|
+
action,
|
|
51
|
+
objectType: 'promotion',
|
|
52
|
+
objectId,
|
|
53
|
+
stateBefore: null,
|
|
54
|
+
stateAfter,
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
async listForPromotion(promotionId) {
|
|
59
|
+
return this.emFactory().find(PromotionCoupon, { promotionId }, { orderBy: { createdAt: 'asc' } });
|
|
60
|
+
}
|
|
61
|
+
async findByCode(code) {
|
|
62
|
+
return this.emFactory().findOne(PromotionCoupon, { code, isActive: true });
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Bulk activate/deactivate coupons by id, scoped to a promotion so an id from
|
|
66
|
+
* another promotion can never be flipped. Returns the number of rows updated.
|
|
67
|
+
*/
|
|
68
|
+
async setActiveBulk(promotionId, couponIds, isActive) {
|
|
69
|
+
if (couponIds.length === 0)
|
|
70
|
+
return 0;
|
|
71
|
+
const em = this.emFactory();
|
|
72
|
+
const coupons = await em.find(PromotionCoupon, {
|
|
73
|
+
id: { $in: couponIds },
|
|
74
|
+
promotionId,
|
|
75
|
+
});
|
|
76
|
+
for (const c of coupons)
|
|
77
|
+
c.isActive = isActive;
|
|
78
|
+
this.#audit(em, 'coupon.set_active_bulk', promotionId, { isActive, count: coupons.length });
|
|
79
|
+
await em.flush();
|
|
80
|
+
return coupons.length;
|
|
81
|
+
}
|
|
82
|
+
/** Create a single specified coupon for a promotion. Rejects duplicates. */
|
|
83
|
+
async createSingle(promotionId, code) {
|
|
84
|
+
const em = this.emFactory();
|
|
85
|
+
const promotion = await em.findOne(Promotion, { id: promotionId });
|
|
86
|
+
if (!promotion) {
|
|
87
|
+
throw new HttpError(404, ERROR_CODES.NOT_FOUND, `Promotion ${promotionId} not found.`);
|
|
88
|
+
}
|
|
89
|
+
const existing = await em.findOne(PromotionCoupon, { code });
|
|
90
|
+
if (existing) {
|
|
91
|
+
throw new HttpError(409, ERROR_CODES.VALIDATION_FAILED, `coupon_code_taken: ${code}`);
|
|
92
|
+
}
|
|
93
|
+
const coupon = em.create(PromotionCoupon, {
|
|
94
|
+
promotionId,
|
|
95
|
+
batchId: null,
|
|
96
|
+
code,
|
|
97
|
+
limitScope: 'per_coupon',
|
|
98
|
+
isActive: true,
|
|
99
|
+
});
|
|
100
|
+
em.persist(coupon);
|
|
101
|
+
this.#audit(em, 'coupon.create_single', promotionId, { code });
|
|
102
|
+
await em.flush();
|
|
103
|
+
return coupon;
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Generate a batch of unique coupon codes (US4). Uniqueness is guaranteed
|
|
107
|
+
* in-batch via a Set and cross-batch via a DB existence check + regenerate;
|
|
108
|
+
* the `code` UNIQUE constraint is the final backstop.
|
|
109
|
+
*/
|
|
110
|
+
async generateBatch(promotionId, req) {
|
|
111
|
+
if (req.count > COUPON_BATCH_MAX) {
|
|
112
|
+
throw new HttpError(400, ERROR_CODES.VALIDATION_FAILED, `batch_too_large: max ${COUPON_BATCH_MAX} per batch`);
|
|
113
|
+
}
|
|
114
|
+
const em = this.emFactory();
|
|
115
|
+
const promotion = await em.findOne(Promotion, { id: promotionId });
|
|
116
|
+
if (!promotion) {
|
|
117
|
+
throw new HttpError(404, ERROR_CODES.NOT_FOUND, `Promotion ${promotionId} not found.`);
|
|
118
|
+
}
|
|
119
|
+
// Generate unique-within-batch codes.
|
|
120
|
+
const codes = new Set();
|
|
121
|
+
let guard = 0;
|
|
122
|
+
const maxAttempts = req.count * 20 + 100;
|
|
123
|
+
while (codes.size < req.count && guard < maxAttempts) {
|
|
124
|
+
codes.add(buildCouponCode(req));
|
|
125
|
+
guard += 1;
|
|
126
|
+
}
|
|
127
|
+
if (codes.size < req.count) {
|
|
128
|
+
throw new HttpError(400, ERROR_CODES.VALIDATION_FAILED, 'coupon_space_exhausted: increase length or widen the format');
|
|
129
|
+
}
|
|
130
|
+
// Drop any that already exist in the DB, then regenerate replacements.
|
|
131
|
+
let candidates = [...codes];
|
|
132
|
+
const existing = await em.find(PromotionCoupon, { code: { $in: candidates } });
|
|
133
|
+
if (existing.length > 0) {
|
|
134
|
+
const taken = new Set(existing.map((c) => c.code));
|
|
135
|
+
const fresh = new Set(candidates.filter((c) => !taken.has(c)));
|
|
136
|
+
while (fresh.size < req.count && guard < maxAttempts) {
|
|
137
|
+
const c = buildCouponCode(req);
|
|
138
|
+
if (!taken.has(c))
|
|
139
|
+
fresh.add(c);
|
|
140
|
+
guard += 1;
|
|
141
|
+
}
|
|
142
|
+
candidates = [...fresh];
|
|
143
|
+
}
|
|
144
|
+
const batch = em.create(CouponBatch, {
|
|
145
|
+
promotionId,
|
|
146
|
+
count: req.count,
|
|
147
|
+
length: req.length,
|
|
148
|
+
format: req.format,
|
|
149
|
+
prefix: req.prefix ?? null,
|
|
150
|
+
suffix: req.suffix ?? null,
|
|
151
|
+
dashEvery: req.dashEvery ?? 0,
|
|
152
|
+
limitScope: req.limitScope,
|
|
153
|
+
});
|
|
154
|
+
// Flush the batch first so the coupons' batch_id FK is satisfied (the
|
|
155
|
+
// coupon entity has no ORM relation, so insert order isn't inferred).
|
|
156
|
+
await em.persistAndFlush(batch);
|
|
157
|
+
for (const code of candidates) {
|
|
158
|
+
em.persist(em.create(PromotionCoupon, {
|
|
159
|
+
promotionId,
|
|
160
|
+
batchId: batch.id,
|
|
161
|
+
code,
|
|
162
|
+
limitScope: req.limitScope,
|
|
163
|
+
isActive: true,
|
|
164
|
+
}));
|
|
165
|
+
}
|
|
166
|
+
this.#audit(em, 'coupon.generate_batch', promotionId, {
|
|
167
|
+
batchId: batch.id,
|
|
168
|
+
generated: candidates.length,
|
|
169
|
+
});
|
|
170
|
+
await em.flush();
|
|
171
|
+
return { batch, generated: candidates.length };
|
|
172
|
+
}
|
|
173
|
+
async listBatchCodes(batchId) {
|
|
174
|
+
const rows = await this.emFactory().find(PromotionCoupon, { batchId }, { orderBy: { createdAt: 'asc' } });
|
|
175
|
+
return rows.map((r) => r.code);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
//# sourceMappingURL=coupon-service.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"coupon-service.js","sourceRoot":"","sources":["../../../src/backend/services/coupon-service.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { PromotionAction } from '@endora-commerce/contracts';
|
|
2
|
+
import type { CartApplyContext, DiscountResult, PromotionActionDefinition } from '../actions/types.js';
|
|
3
|
+
/**
|
|
4
|
+
* Registry of promotion actions (FR-012). The 10 built-ins register at
|
|
5
|
+
* construction; other modules contribute additional action types via
|
|
6
|
+
* `register(...)` during composition — the same extension-point shape used
|
|
7
|
+
* by the payment / shipping adapter registries.
|
|
8
|
+
*/
|
|
9
|
+
export declare class PromotionActionRegistry {
|
|
10
|
+
private readonly defs;
|
|
11
|
+
register<C extends PromotionAction>(def: PromotionActionDefinition<C>): void;
|
|
12
|
+
get(type: string): PromotionActionDefinition | null;
|
|
13
|
+
list(): PromotionActionDefinition[];
|
|
14
|
+
has(type: string): boolean;
|
|
15
|
+
/** Apply a configured action against a cart context. Throws on unknown type. */
|
|
16
|
+
apply(action: PromotionAction, ctx: CartApplyContext): DiscountResult;
|
|
17
|
+
}
|
|
18
|
+
/** Build a registry pre-loaded with all built-in actions. */
|
|
19
|
+
export declare function createPromotionActionRegistry(): PromotionActionRegistry;
|
|
20
|
+
//# sourceMappingURL=promotion-action-registry.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"promotion-action-registry.d.ts","sourceRoot":"","sources":["../../../src/backend/services/promotion-action-registry.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,4BAA4B,CAAC;AAClE,OAAO,KAAK,EAAE,gBAAgB,EAAE,cAAc,EAAE,yBAAyB,EAAE,MAAM,qBAAqB,CAAC;AAYvG;;;;;GAKG;AACH,qBAAa,uBAAuB;IAClC,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAgD;IAErE,QAAQ,CAAC,CAAC,SAAS,eAAe,EAAE,GAAG,EAAE,yBAAyB,CAAC,CAAC,CAAC,GAAG,IAAI;IAO5E,GAAG,CAAC,IAAI,EAAE,MAAM,GAAG,yBAAyB,GAAG,IAAI;IAInD,IAAI,IAAI,yBAAyB,EAAE;IAInC,GAAG,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO;IAI1B,gFAAgF;IAChF,KAAK,CAAC,MAAM,EAAE,eAAe,EAAE,GAAG,EAAE,gBAAgB,GAAG,cAAc;CAKtE;AAED,6DAA6D;AAC7D,wBAAgB,6BAA6B,IAAI,uBAAuB,CAavE"}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { freeDeliveryAction } from '../actions/free-delivery.js';
|
|
2
|
+
import { percentageOffCartAction } from '../actions/percentage-off-cart.js';
|
|
3
|
+
import { amountOffCartAction } from '../actions/amount-off-cart.js';
|
|
4
|
+
import { buyXGetYFreeAction } from '../actions/buy-x-get-y-free.js';
|
|
5
|
+
import { spendXPercentOffAction } from '../actions/spend-x-percent-off.js';
|
|
6
|
+
import { spendXAmountOffAction } from '../actions/spend-x-amount-off.js';
|
|
7
|
+
import { everyNthProductPercentOffAction } from '../actions/every-nth-product-percent-off.js';
|
|
8
|
+
import { buyXUnitsYFreeAction } from '../actions/buy-x-units-y-free.js';
|
|
9
|
+
import { buyXUnitsPercentOffAction } from '../actions/buy-x-units-percent-off.js';
|
|
10
|
+
import { buyXUnitsAmountOffAction } from '../actions/buy-x-units-amount-off.js';
|
|
11
|
+
/**
|
|
12
|
+
* Registry of promotion actions (FR-012). The 10 built-ins register at
|
|
13
|
+
* construction; other modules contribute additional action types via
|
|
14
|
+
* `register(...)` during composition — the same extension-point shape used
|
|
15
|
+
* by the payment / shipping adapter registries.
|
|
16
|
+
*/
|
|
17
|
+
export class PromotionActionRegistry {
|
|
18
|
+
defs = new Map();
|
|
19
|
+
register(def) {
|
|
20
|
+
// Definitions are keyed by their action `type`. The cast widens the
|
|
21
|
+
// per-variant config back to the union so heterogeneous definitions can
|
|
22
|
+
// live in one map; `apply` is only ever invoked with the matching variant.
|
|
23
|
+
this.defs.set(def.type, def);
|
|
24
|
+
}
|
|
25
|
+
get(type) {
|
|
26
|
+
return this.defs.get(type) ?? null;
|
|
27
|
+
}
|
|
28
|
+
list() {
|
|
29
|
+
return [...this.defs.values()];
|
|
30
|
+
}
|
|
31
|
+
has(type) {
|
|
32
|
+
return this.defs.has(type);
|
|
33
|
+
}
|
|
34
|
+
/** Apply a configured action against a cart context. Throws on unknown type. */
|
|
35
|
+
apply(action, ctx) {
|
|
36
|
+
const def = this.defs.get(action.type);
|
|
37
|
+
if (!def)
|
|
38
|
+
throw new Error(`unknown_promotion_action: ${action.type}`);
|
|
39
|
+
return def.apply(action, ctx);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
/** Build a registry pre-loaded with all built-in actions. */
|
|
43
|
+
export function createPromotionActionRegistry() {
|
|
44
|
+
const registry = new PromotionActionRegistry();
|
|
45
|
+
registry.register(freeDeliveryAction);
|
|
46
|
+
registry.register(percentageOffCartAction);
|
|
47
|
+
registry.register(amountOffCartAction);
|
|
48
|
+
registry.register(buyXGetYFreeAction);
|
|
49
|
+
registry.register(spendXPercentOffAction);
|
|
50
|
+
registry.register(spendXAmountOffAction);
|
|
51
|
+
registry.register(everyNthProductPercentOffAction);
|
|
52
|
+
registry.register(buyXUnitsYFreeAction);
|
|
53
|
+
registry.register(buyXUnitsPercentOffAction);
|
|
54
|
+
registry.register(buyXUnitsAmountOffAction);
|
|
55
|
+
return registry;
|
|
56
|
+
}
|
|
57
|
+
//# sourceMappingURL=promotion-action-registry.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"promotion-action-registry.js","sourceRoot":"","sources":["../../../src/backend/services/promotion-action-registry.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,kBAAkB,EAAE,MAAM,6BAA6B,CAAC;AACjE,OAAO,EAAE,uBAAuB,EAAE,MAAM,mCAAmC,CAAC;AAC5E,OAAO,EAAE,mBAAmB,EAAE,MAAM,+BAA+B,CAAC;AACpE,OAAO,EAAE,kBAAkB,EAAE,MAAM,gCAAgC,CAAC;AACpE,OAAO,EAAE,sBAAsB,EAAE,MAAM,mCAAmC,CAAC;AAC3E,OAAO,EAAE,qBAAqB,EAAE,MAAM,kCAAkC,CAAC;AACzE,OAAO,EAAE,+BAA+B,EAAE,MAAM,6CAA6C,CAAC;AAC9F,OAAO,EAAE,oBAAoB,EAAE,MAAM,kCAAkC,CAAC;AACxE,OAAO,EAAE,yBAAyB,EAAE,MAAM,uCAAuC,CAAC;AAClF,OAAO,EAAE,wBAAwB,EAAE,MAAM,sCAAsC,CAAC;AAEhF;;;;;GAKG;AACH,MAAM,OAAO,uBAAuB;IACjB,IAAI,GAAG,IAAI,GAAG,EAAqC,CAAC;IAErE,QAAQ,CAA4B,GAAiC;QACnE,oEAAoE;QACpE,wEAAwE;QACxE,2EAA2E;QAC3E,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,IAAI,EAAE,GAA2C,CAAC,CAAC;IACvE,CAAC;IAED,GAAG,CAAC,IAAY;QACd,OAAO,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,IAAI,CAAC;IACrC,CAAC;IAED,IAAI;QACF,OAAO,CAAC,GAAG,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE,CAAC,CAAC;IACjC,CAAC;IAED,GAAG,CAAC,IAAY;QACd,OAAO,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;IAC7B,CAAC;IAED,gFAAgF;IAChF,KAAK,CAAC,MAAuB,EAAE,GAAqB;QAClD,MAAM,GAAG,GAAG,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;QACvC,IAAI,CAAC,GAAG;YAAE,MAAM,IAAI,KAAK,CAAC,6BAA6B,MAAM,CAAC,IAAI,EAAE,CAAC,CAAC;QACtE,OAAO,GAAG,CAAC,KAAK,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;IAChC,CAAC;CACF;AAED,6DAA6D;AAC7D,MAAM,UAAU,6BAA6B;IAC3C,MAAM,QAAQ,GAAG,IAAI,uBAAuB,EAAE,CAAC;IAC/C,QAAQ,CAAC,QAAQ,CAAC,kBAAkB,CAAC,CAAC;IACtC,QAAQ,CAAC,QAAQ,CAAC,uBAAuB,CAAC,CAAC;IAC3C,QAAQ,CAAC,QAAQ,CAAC,mBAAmB,CAAC,CAAC;IACvC,QAAQ,CAAC,QAAQ,CAAC,kBAAkB,CAAC,CAAC;IACtC,QAAQ,CAAC,QAAQ,CAAC,sBAAsB,CAAC,CAAC;IAC1C,QAAQ,CAAC,QAAQ,CAAC,qBAAqB,CAAC,CAAC;IACzC,QAAQ,CAAC,QAAQ,CAAC,+BAA+B,CAAC,CAAC;IACnD,QAAQ,CAAC,QAAQ,CAAC,oBAAoB,CAAC,CAAC;IACxC,QAAQ,CAAC,QAAQ,CAAC,yBAAyB,CAAC,CAAC;IAC7C,QAAQ,CAAC,QAAQ,CAAC,wBAAwB,CAAC,CAAC;IAC5C,OAAO,QAAQ,CAAC;AAClB,CAAC"}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { EntityManager } from '@mikro-orm/postgresql';
|
|
2
|
+
import type { PromotionCodePort, ResolvedPromotionCode } from '@endora-commerce/contracts';
|
|
3
|
+
/**
|
|
4
|
+
* The coupon-resolution port `promotions` publishes (feature 075, Phase P).
|
|
5
|
+
*
|
|
6
|
+
* `carts` reaches both entity classes for exactly one question — "is this code
|
|
7
|
+
* live, and which promotion is it?" — across a three-step lookup: the legacy
|
|
8
|
+
* `promotions.code` column first, then the `promotion_coupons` table, then the
|
|
9
|
+
* promotion behind the coupon, each filtered on `isActive`. That order and
|
|
10
|
+
* those filters are this module's business, and reproducing them in `carts`
|
|
11
|
+
* meant reproducing them correctly.
|
|
12
|
+
*
|
|
13
|
+
* The answer is eight fields rather than the thirty-column row, because eight
|
|
14
|
+
* is what the question needs: which promotion, and — when the buyer may not
|
|
15
|
+
* use it — why not. The four eligibility fields joined the record in feature
|
|
16
|
+
* 075's `carts` cut; without them the caller could only answer `invalid_code`
|
|
17
|
+
* where it answers `expired`, `wrong_organization` and `below_min_spend`.
|
|
18
|
+
*/
|
|
19
|
+
export declare class PromotionCodeService implements PromotionCodePort {
|
|
20
|
+
private readonly emFactory;
|
|
21
|
+
constructor(emFactory: () => EntityManager);
|
|
22
|
+
resolveByCode(code: string): Promise<ResolvedPromotionCode | null>;
|
|
23
|
+
}
|
|
24
|
+
//# sourceMappingURL=promotion-code-port.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"promotion-code-port.d.ts","sourceRoot":"","sources":["../../../src/backend/services/promotion-code-port.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;AAC3D,OAAO,KAAK,EAAE,iBAAiB,EAAE,qBAAqB,EAAE,MAAM,4BAA4B,CAAC;AAI3F;;;;;;;;;;;;;;;GAeG;AACH,qBAAa,oBAAqB,YAAW,iBAAiB;IAChD,OAAO,CAAC,QAAQ,CAAC,SAAS;gBAAT,SAAS,EAAE,MAAM,aAAa;IAErD,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,qBAAqB,GAAG,IAAI,CAAC;CAiBzE"}
|