@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
package/dist/manifest.js
ADDED
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { defineModuleManifest } from '@endora-commerce/contracts';
|
|
2
|
+
/**
|
|
3
|
+
* Promotions module — rules engine (feature 045).
|
|
4
|
+
*
|
|
5
|
+
* Owns promotion definitions, the typed rule builder, the pluggable action
|
|
6
|
+
* catalogue, coupons + generator, usage limits, and usage statistics.
|
|
7
|
+
*/
|
|
8
|
+
export const PROMOTION_PERMISSIONS = {
|
|
9
|
+
READ: 'promotions:read',
|
|
10
|
+
WRITE: 'promotions:write',
|
|
11
|
+
DELETE: 'promotions:delete',
|
|
12
|
+
};
|
|
13
|
+
export const manifest = defineModuleManifest({
|
|
14
|
+
id: 'promotions',
|
|
15
|
+
name: 'Promotions',
|
|
16
|
+
description: 'Promotion rules engine: rule builder, actions, coupons, limits, statistics.',
|
|
17
|
+
version: '1.0.0',
|
|
18
|
+
// `carts` is deliberately NOT declared, despite the rule builder reading it
|
|
19
|
+
// at runtime: carts applies promotions through
|
|
20
|
+
// PromotionService.applyToCart(CartSnapshot) — a value object — so that edge
|
|
21
|
+
// runs the other way.
|
|
22
|
+
//
|
|
23
|
+
// `orders` used to sit beside it under the same sentence, and D-94.1 moves
|
|
24
|
+
// it: `promotion_usages_order_fk` (`promotion_usages.order_id` ->
|
|
25
|
+
// `orders.id`, `on delete restrict`) is a cross-module foreign key, and
|
|
26
|
+
// AGENTS.md § Migrations item 4 says such an edge is declared here or the
|
|
27
|
+
// build fails — an `acknowledgedDependencies` entry does not satisfy it. The
|
|
28
|
+
// edge is mutual: `placeOrder` calls `finalizeUsage(tx, …)` on this module's
|
|
29
|
+
// finalizer and this module writes a redemption row against the order. The
|
|
30
|
+
// cycle that closes (`promotions -> orders -> carts -> promotions`) is
|
|
31
|
+
// broken on the other side — `orders` **and** `carts` re-express
|
|
32
|
+
// `promotionService` as an acknowledged edge, which drops the install
|
|
33
|
+
// ordering the constraint says is backwards and keeps the bind (D-94.3).
|
|
34
|
+
//
|
|
35
|
+
// The remaining former declarations (organizations, price_lists,
|
|
36
|
+
// payment_methods, delivery_methods, dictionaries) are rule *dimensions* the
|
|
37
|
+
// builder offers, not install-time necessities: a promotion module with no
|
|
38
|
+
// price lists installed simply offers fewer conditions. `dependencies` means
|
|
39
|
+
// "cannot exist without" — specs/065-manifest-aware-migrations/research.md §R9.
|
|
40
|
+
// `auth` owns `requireAdmin`; `dictionaries` owns the language-scope
|
|
41
|
+
// validator; `organizations` owns the status gate that keeps a suspended
|
|
42
|
+
// organization from collecting org-targeted discounts. Feature 072 made all
|
|
43
|
+
// three container resolutions rather than optional arguments.
|
|
44
|
+
// `currencies` owns `currencyReferenceRegistry`, the registry this module
|
|
45
|
+
// contributes its "who still prices in this currency" descriptor to (feature
|
|
46
|
+
// 077, D-87). `currencies` used to ask the question itself, with a
|
|
47
|
+
// `count(*) from "promotions"` naming this module's table — an edge that
|
|
48
|
+
// existed nowhere an operator, the lifecycle or the migration order could see.
|
|
49
|
+
dependencies: [
|
|
50
|
+
'catalog',
|
|
51
|
+
'sales_channels',
|
|
52
|
+
'auth',
|
|
53
|
+
'currencies',
|
|
54
|
+
'dictionaries',
|
|
55
|
+
'orders',
|
|
56
|
+
'organizations',
|
|
57
|
+
],
|
|
58
|
+
settings: {
|
|
59
|
+
moduleCode: 'promotions',
|
|
60
|
+
groups: [{ code: 'promotions', name: 'Promotions' }],
|
|
61
|
+
settings: [
|
|
62
|
+
{
|
|
63
|
+
// Feature 073 — the operator's activation control. Platform-wide.
|
|
64
|
+
code: 'promotions.enabled',
|
|
65
|
+
name: 'Promotions enabled',
|
|
66
|
+
description: 'Switches promotions, coupons and the Rule Builder on or off, including the discounts they apply at checkout. Nothing is dropped: promotions, their rules and their usage history stay in the database and apply again exactly as configured when you switch it back on.',
|
|
67
|
+
groupCode: 'promotions',
|
|
68
|
+
valueType: 'boolean',
|
|
69
|
+
defaultValue: true,
|
|
70
|
+
},
|
|
71
|
+
],
|
|
72
|
+
},
|
|
73
|
+
/**
|
|
74
|
+
* `PROMOTION_INVALID` — D-129's remaining sweep, MR 4
|
|
75
|
+
* (`specs/090-module-owned-error-codes/d129-sweep.md` §5.2, Appendix A;
|
|
76
|
+
* D-186 in `specs/080-f4-real-scope/rulings.md`). The first error code this
|
|
77
|
+
* module declares.
|
|
78
|
+
*
|
|
79
|
+
* It was `_i18n`'s until now, not because anybody judged it the platform's
|
|
80
|
+
* but because the deleted prefix chain had no rule for it and its last line
|
|
81
|
+
* was `return 'core'`. **D-121 T1 puts it here**: the noun is a promotion,
|
|
82
|
+
* which is this module's own entity, its rules and its Rule Builder.
|
|
83
|
+
*
|
|
84
|
+
* **Nothing in the tree raises it** (class D), and the one place it is named
|
|
85
|
+
* outside a declaration says so: `carts`' manifest records it as a code a
|
|
86
|
+
* reader will look for there and not find, because it "reads like the coupon
|
|
87
|
+
* path" and falls through the chain. `carts` raises `CART_COUPON_REJECTED`
|
|
88
|
+
* with a `details.code` drawn from `couponDropReasonSchema` for every coupon
|
|
89
|
+
* refusal it makes, so the cart path is answered and this code is not a gap
|
|
90
|
+
* in it — it is a member of `ERROR_CODES` no rule has ever produced.
|
|
91
|
+
*
|
|
92
|
+
* **It therefore arrives without a sentence, deliberately.** It carried a
|
|
93
|
+
* placeholder in `_i18n`'s bundle, the code rewritten twice —
|
|
94
|
+
* `"Promotion Invalid."` and `"Błąd: promotion invalid."` — which D-186 §2
|
|
95
|
+
* deletes rather than moves: in this module's own bundle it would read as this
|
|
96
|
+
* module's answer and every instrument would count the code as translated for
|
|
97
|
+
* good. `d129-sweep.md` §5.4 keeps writing real prose available and calls it
|
|
98
|
+
* the better outcome, and it is available whenever a raise site says what the
|
|
99
|
+
* refusal means. There is none, so a sentence could only be invented from the
|
|
100
|
+
* code's own name — the placeholder again in longer words, rendered for
|
|
101
|
+
* nobody. It is a `check-error-translations.ts` `UNTRANSLATED_ERROR_CODES`
|
|
102
|
+
* entry under `promotions` instead, where the debt is findable and attached
|
|
103
|
+
* to whoever gives the code a raise site.
|
|
104
|
+
*
|
|
105
|
+
* No `tokens`, for the same reason and from the same direction: there is no
|
|
106
|
+
* raise site to put a `details.code` on the wire.
|
|
107
|
+
*/
|
|
108
|
+
errorCodes: [{ code: 'PROMOTION_INVALID' }],
|
|
109
|
+
i18n: { bundlesDir: 'i18n' },
|
|
110
|
+
docs: { dir: 'docs' },
|
|
111
|
+
permissions: [
|
|
112
|
+
{ code: PROMOTION_PERMISSIONS.READ, label: 'View promotions' },
|
|
113
|
+
{ code: PROMOTION_PERMISSIONS.WRITE, label: 'Create + edit promotions and rules' },
|
|
114
|
+
{ code: PROMOTION_PERMISSIONS.DELETE, label: 'Delete promotions and rules' },
|
|
115
|
+
],
|
|
116
|
+
actions: [
|
|
117
|
+
{
|
|
118
|
+
id: 'open-promotions',
|
|
119
|
+
labelKey: 'actions.openPromotions.label',
|
|
120
|
+
descriptionKey: 'actions.openPromotions.description',
|
|
121
|
+
icon: 'Tag',
|
|
122
|
+
targetRoute: '/promotions',
|
|
123
|
+
requiredPermission: PROMOTION_PERMISSIONS.READ,
|
|
124
|
+
keywords: ['promotion', 'promotions', 'discount', 'coupon', 'marketing'],
|
|
125
|
+
weight: 250,
|
|
126
|
+
},
|
|
127
|
+
{
|
|
128
|
+
id: 'new-promotion',
|
|
129
|
+
labelKey: 'actions.newPromotion.label',
|
|
130
|
+
descriptionKey: 'actions.newPromotion.description',
|
|
131
|
+
icon: 'Plus',
|
|
132
|
+
targetRoute: '/promotions/new',
|
|
133
|
+
requiredPermission: PROMOTION_PERMISSIONS.WRITE,
|
|
134
|
+
keywords: ['promotion', 'new', 'create', 'discount', 'coupon'],
|
|
135
|
+
weight: 251,
|
|
136
|
+
},
|
|
137
|
+
],
|
|
138
|
+
activation: { settingCode: 'promotions.enabled', default: true },
|
|
139
|
+
});
|
|
140
|
+
//# sourceMappingURL=manifest.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"manifest.js","sourceRoot":"","sources":["../src/manifest.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { Migration } from '@mikro-orm/migrations';
|
|
2
|
+
/**
|
|
3
|
+
* Promotions feature 012 / US8 — add the JSONB `criteria` column to
|
|
4
|
+
* `promotions`.
|
|
5
|
+
*
|
|
6
|
+
* The column carries a discriminated-union array of line-level criteria
|
|
7
|
+
* (today only the `attribute` variant is exercised). It is ANDed with
|
|
8
|
+
* the existing flat `category_id` / `product_id` scope: a cart line is
|
|
9
|
+
* included in a promotion's `lineBase` only if it satisfies both the
|
|
10
|
+
* legacy scope and every criterion.
|
|
11
|
+
*
|
|
12
|
+
* Default `'[]'::jsonb` so every existing promotion keeps its current
|
|
13
|
+
* behaviour without operator action.
|
|
14
|
+
*/
|
|
15
|
+
export declare class Migration20260505T074605PromotionsCriteria extends Migration {
|
|
16
|
+
up(): Promise<void>;
|
|
17
|
+
down(): Promise<void>;
|
|
18
|
+
}
|
|
19
|
+
//# sourceMappingURL=20260505T074605_promotions_criteria.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"20260505T074605_promotions_criteria.d.ts","sourceRoot":"","sources":["../../src/migrations/20260505T074605_promotions_criteria.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,uBAAuB,CAAC;AAElD;;;;;;;;;;;;GAYG;AACH,qBAAa,0CAA2C,SAAQ,SAAS;IACxD,EAAE,IAAI,OAAO,CAAC,IAAI,CAAC;IAMnB,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;CAGrC"}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { Migration } from '@mikro-orm/migrations';
|
|
2
|
+
/**
|
|
3
|
+
* Promotions feature 012 / US8 — add the JSONB `criteria` column to
|
|
4
|
+
* `promotions`.
|
|
5
|
+
*
|
|
6
|
+
* The column carries a discriminated-union array of line-level criteria
|
|
7
|
+
* (today only the `attribute` variant is exercised). It is ANDed with
|
|
8
|
+
* the existing flat `category_id` / `product_id` scope: a cart line is
|
|
9
|
+
* included in a promotion's `lineBase` only if it satisfies both the
|
|
10
|
+
* legacy scope and every criterion.
|
|
11
|
+
*
|
|
12
|
+
* Default `'[]'::jsonb` so every existing promotion keeps its current
|
|
13
|
+
* behaviour without operator action.
|
|
14
|
+
*/
|
|
15
|
+
export class Migration20260505T074605PromotionsCriteria extends Migration {
|
|
16
|
+
async up() {
|
|
17
|
+
this.addSql(`alter table "promotions" add column "criteria" jsonb not null default '[]'::jsonb;`);
|
|
18
|
+
}
|
|
19
|
+
async down() {
|
|
20
|
+
this.addSql(`alter table "promotions" drop column "criteria";`);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
//# sourceMappingURL=20260505T074605_promotions_criteria.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"20260505T074605_promotions_criteria.js","sourceRoot":"","sources":["../../src/migrations/20260505T074605_promotions_criteria.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,uBAAuB,CAAC;AAElD;;;;;;;;;;;;GAYG;AACH,MAAM,OAAO,0CAA2C,SAAQ,SAAS;IAC9D,KAAK,CAAC,EAAE;QACf,IAAI,CAAC,MAAM,CACT,oFAAoF,CACrF,CAAC;IACJ,CAAC;IAEQ,KAAK,CAAC,IAAI;QACjB,IAAI,CAAC,MAAM,CAAC,kDAAkD,CAAC,CAAC;IAClE,CAAC;CACF"}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { Migration } from '@mikro-orm/migrations';
|
|
2
|
+
/**
|
|
3
|
+
* Feature 045 — promotions rules engine.
|
|
4
|
+
*
|
|
5
|
+
* Schema changes (one atomic migration):
|
|
6
|
+
* - ALTER `promotions`: relax `kind` / `value` to nullable (action-based
|
|
7
|
+
* promotions carry no legacy effect); add engine columns (description,
|
|
8
|
+
* priority, stop_further, action_type, action_config, rule_id,
|
|
9
|
+
* rule_definition, usage limits).
|
|
10
|
+
* - new tables: promotion_rules, coupon_batches, promotion_coupons,
|
|
11
|
+
* promotion_usages, promotion_usage_counters (with indexes/uniques per
|
|
12
|
+
* data-model.md).
|
|
13
|
+
* - backfill legacy `promotions.code` into a `promotion_coupons` row.
|
|
14
|
+
*/
|
|
15
|
+
export declare class Migration20260618T100727PromotionsEngine extends Migration {
|
|
16
|
+
up(): Promise<void>;
|
|
17
|
+
down(): Promise<void>;
|
|
18
|
+
}
|
|
19
|
+
//# sourceMappingURL=20260618T100727_promotions_engine.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"20260618T100727_promotions_engine.d.ts","sourceRoot":"","sources":["../../src/migrations/20260618T100727_promotions_engine.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,uBAAuB,CAAC;AAElD;;;;;;;;;;;;GAYG;AACH,qBAAa,wCAAyC,SAAQ,SAAS;IACtD,EAAE,IAAI,OAAO,CAAC,IAAI,CAAC;IAiInB,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;CAqBrC"}
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import { Migration } from '@mikro-orm/migrations';
|
|
2
|
+
/**
|
|
3
|
+
* Feature 045 — promotions rules engine.
|
|
4
|
+
*
|
|
5
|
+
* Schema changes (one atomic migration):
|
|
6
|
+
* - ALTER `promotions`: relax `kind` / `value` to nullable (action-based
|
|
7
|
+
* promotions carry no legacy effect); add engine columns (description,
|
|
8
|
+
* priority, stop_further, action_type, action_config, rule_id,
|
|
9
|
+
* rule_definition, usage limits).
|
|
10
|
+
* - new tables: promotion_rules, coupon_batches, promotion_coupons,
|
|
11
|
+
* promotion_usages, promotion_usage_counters (with indexes/uniques per
|
|
12
|
+
* data-model.md).
|
|
13
|
+
* - backfill legacy `promotions.code` into a `promotion_coupons` row.
|
|
14
|
+
*/
|
|
15
|
+
export class Migration20260618T100727PromotionsEngine extends Migration {
|
|
16
|
+
async up() {
|
|
17
|
+
// 1) promotions — relax legacy columns + add engine columns
|
|
18
|
+
this.addSql(`alter table "promotions" alter column "kind" drop not null;`);
|
|
19
|
+
this.addSql(`alter table "promotions" alter column "value" drop not null;`);
|
|
20
|
+
this.addSql(`alter table "promotions" add column "description" text null;`);
|
|
21
|
+
this.addSql(`alter table "promotions" add column "priority" integer not null default 0;`);
|
|
22
|
+
this.addSql(`alter table "promotions" add column "stop_further" boolean not null default false;`);
|
|
23
|
+
this.addSql(`alter table "promotions" add column "action_type" varchar(64) null;`);
|
|
24
|
+
this.addSql(`alter table "promotions" add column "action_config" jsonb not null default '{}';`);
|
|
25
|
+
this.addSql(`alter table "promotions" add column "rule_id" uuid null;`);
|
|
26
|
+
this.addSql(`alter table "promotions" add column "rule_definition" jsonb null;`);
|
|
27
|
+
this.addSql(`alter table "promotions" add column "usage_limit_global" integer null;`);
|
|
28
|
+
this.addSql(`alter table "promotions" add column "usage_limit_per_organization" integer null;`);
|
|
29
|
+
this.addSql(`alter table "promotions" add column "usage_limit_per_customer" integer null;`);
|
|
30
|
+
this.addSql(`create index "idx_promotions_priority" on "promotions" ("priority");`);
|
|
31
|
+
this.addSql(`create index "idx_promotions_rule_id" on "promotions" ("rule_id");`);
|
|
32
|
+
this.addSql(`create index "idx_promotions_validity" on "promotions" ("valid_from", "valid_until");`);
|
|
33
|
+
// 2) promotion_rules — standalone named rules
|
|
34
|
+
this.addSql(`
|
|
35
|
+
create table "promotion_rules" (
|
|
36
|
+
"id" uuid not null,
|
|
37
|
+
"name" varchar(160) not null,
|
|
38
|
+
"description" text null,
|
|
39
|
+
"definition" jsonb not null,
|
|
40
|
+
"created_at" timestamptz not null,
|
|
41
|
+
"updated_at" timestamptz not null,
|
|
42
|
+
constraint "promotion_rules_pkey" primary key ("id")
|
|
43
|
+
);
|
|
44
|
+
`);
|
|
45
|
+
this.addSql(`alter table "promotion_rules" add constraint "promotion_rules_name_unique" unique ("name");`);
|
|
46
|
+
this.addSql(`alter table "promotions" add constraint "promotions_rule_id_foreign" foreign key ("rule_id") references "promotion_rules" ("id") on update cascade on delete set null;`);
|
|
47
|
+
// 3) coupon_batches — generator config
|
|
48
|
+
this.addSql(`
|
|
49
|
+
create table "coupon_batches" (
|
|
50
|
+
"id" uuid not null,
|
|
51
|
+
"promotion_id" uuid not null,
|
|
52
|
+
"count" integer not null,
|
|
53
|
+
"length" integer not null,
|
|
54
|
+
"format" varchar(16) not null,
|
|
55
|
+
"prefix" varchar(32) null,
|
|
56
|
+
"suffix" varchar(32) null,
|
|
57
|
+
"dash_every" integer not null default 0,
|
|
58
|
+
"limit_scope" varchar(16) not null,
|
|
59
|
+
"created_at" timestamptz not null,
|
|
60
|
+
constraint "coupon_batches_pkey" primary key ("id")
|
|
61
|
+
);
|
|
62
|
+
`);
|
|
63
|
+
this.addSql(`create index "idx_coupon_batches_promotion" on "coupon_batches" ("promotion_id");`);
|
|
64
|
+
this.addSql(`alter table "coupon_batches" add constraint "coupon_batches_promotion_id_foreign" foreign key ("promotion_id") references "promotions" ("id") on update cascade on delete cascade;`);
|
|
65
|
+
// 4) promotion_coupons — individual codes
|
|
66
|
+
this.addSql(`
|
|
67
|
+
create table "promotion_coupons" (
|
|
68
|
+
"id" uuid not null,
|
|
69
|
+
"promotion_id" uuid not null,
|
|
70
|
+
"batch_id" uuid null,
|
|
71
|
+
"code" varchar(80) not null,
|
|
72
|
+
"limit_scope" varchar(16) not null,
|
|
73
|
+
"is_active" boolean not null default true,
|
|
74
|
+
"created_at" timestamptz not null,
|
|
75
|
+
constraint "promotion_coupons_pkey" primary key ("id")
|
|
76
|
+
);
|
|
77
|
+
`);
|
|
78
|
+
this.addSql(`alter table "promotion_coupons" add constraint "promotion_coupons_code_unique" unique ("code");`);
|
|
79
|
+
this.addSql(`create index "idx_promotion_coupons_promotion" on "promotion_coupons" ("promotion_id");`);
|
|
80
|
+
this.addSql(`create index "idx_promotion_coupons_batch" on "promotion_coupons" ("batch_id");`);
|
|
81
|
+
this.addSql(`alter table "promotion_coupons" add constraint "promotion_coupons_promotion_id_foreign" foreign key ("promotion_id") references "promotions" ("id") on update cascade on delete cascade;`);
|
|
82
|
+
this.addSql(`alter table "promotion_coupons" add constraint "promotion_coupons_batch_id_foreign" foreign key ("batch_id") references "coupon_batches" ("id") on update cascade on delete set null;`);
|
|
83
|
+
// 5) promotion_usages — finalized redemptions (statistics source)
|
|
84
|
+
this.addSql(`
|
|
85
|
+
create table "promotion_usages" (
|
|
86
|
+
"id" uuid not null,
|
|
87
|
+
"promotion_id" uuid not null,
|
|
88
|
+
"coupon_id" uuid null,
|
|
89
|
+
"order_id" uuid not null,
|
|
90
|
+
"customer_account_id" uuid null,
|
|
91
|
+
"organization_id" uuid null,
|
|
92
|
+
"customer_group_id" uuid null,
|
|
93
|
+
"sales_channel_id" uuid not null,
|
|
94
|
+
"discount_amount" numeric(14,2) not null,
|
|
95
|
+
"currency" varchar(3) not null,
|
|
96
|
+
"created_at" timestamptz not null,
|
|
97
|
+
constraint "promotion_usages_pkey" primary key ("id")
|
|
98
|
+
);
|
|
99
|
+
`);
|
|
100
|
+
this.addSql(`alter table "promotion_usages" add constraint "promotion_usages_order_promotion_unique" unique ("order_id", "promotion_id");`);
|
|
101
|
+
this.addSql(`create index "idx_promotion_usages_promotion" on "promotion_usages" ("promotion_id", "created_at");`);
|
|
102
|
+
this.addSql(`create index "idx_promotion_usages_coupon" on "promotion_usages" ("coupon_id");`);
|
|
103
|
+
this.addSql(`create index "idx_promotion_usages_customer" on "promotion_usages" ("customer_account_id");`);
|
|
104
|
+
this.addSql(`create index "idx_promotion_usages_organization" on "promotion_usages" ("organization_id");`);
|
|
105
|
+
this.addSql(`create index "idx_promotion_usages_group" on "promotion_usages" ("customer_group_id");`);
|
|
106
|
+
this.addSql(`create index "idx_promotion_usages_channel" on "promotion_usages" ("sales_channel_id");`);
|
|
107
|
+
// 6) promotion_usage_counters — atomic cap enforcement
|
|
108
|
+
this.addSql(`
|
|
109
|
+
create table "promotion_usage_counters" (
|
|
110
|
+
"id" uuid not null,
|
|
111
|
+
"scope_type" varchar(16) not null,
|
|
112
|
+
"scope_key" varchar(128) not null,
|
|
113
|
+
"count" integer not null default 0,
|
|
114
|
+
constraint "promotion_usage_counters_pkey" primary key ("id")
|
|
115
|
+
);
|
|
116
|
+
`);
|
|
117
|
+
this.addSql(`alter table "promotion_usage_counters" add constraint "promotion_usage_counters_scope_unique" unique ("scope_type", "scope_key");`);
|
|
118
|
+
// 7) backfill legacy promotions.code → promotion_coupons
|
|
119
|
+
this.addSql(`
|
|
120
|
+
insert into "promotion_coupons" ("id", "promotion_id", "batch_id", "code", "limit_scope", "is_active", "created_at")
|
|
121
|
+
select gen_random_uuid(), "id", null, "code", 'per_coupon', true, now()
|
|
122
|
+
from "promotions"
|
|
123
|
+
where "code" is not null;
|
|
124
|
+
`);
|
|
125
|
+
}
|
|
126
|
+
async down() {
|
|
127
|
+
this.addSql(`drop table if exists "promotion_usage_counters";`);
|
|
128
|
+
this.addSql(`drop table if exists "promotion_usages";`);
|
|
129
|
+
this.addSql(`drop table if exists "promotion_coupons";`);
|
|
130
|
+
this.addSql(`drop table if exists "coupon_batches";`);
|
|
131
|
+
this.addSql(`alter table "promotions" drop constraint if exists "promotions_rule_id_foreign";`);
|
|
132
|
+
this.addSql(`drop table if exists "promotion_rules";`);
|
|
133
|
+
this.addSql(`drop index if exists "idx_promotions_priority";`);
|
|
134
|
+
this.addSql(`drop index if exists "idx_promotions_rule_id";`);
|
|
135
|
+
this.addSql(`drop index if exists "idx_promotions_validity";`);
|
|
136
|
+
this.addSql(`alter table "promotions" drop column "description";`);
|
|
137
|
+
this.addSql(`alter table "promotions" drop column "priority";`);
|
|
138
|
+
this.addSql(`alter table "promotions" drop column "stop_further";`);
|
|
139
|
+
this.addSql(`alter table "promotions" drop column "action_type";`);
|
|
140
|
+
this.addSql(`alter table "promotions" drop column "action_config";`);
|
|
141
|
+
this.addSql(`alter table "promotions" drop column "rule_id";`);
|
|
142
|
+
this.addSql(`alter table "promotions" drop column "rule_definition";`);
|
|
143
|
+
this.addSql(`alter table "promotions" drop column "usage_limit_global";`);
|
|
144
|
+
this.addSql(`alter table "promotions" drop column "usage_limit_per_organization";`);
|
|
145
|
+
this.addSql(`alter table "promotions" drop column "usage_limit_per_customer";`);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
//# sourceMappingURL=20260618T100727_promotions_engine.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"20260618T100727_promotions_engine.js","sourceRoot":"","sources":["../../src/migrations/20260618T100727_promotions_engine.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,uBAAuB,CAAC;AAElD;;;;;;;;;;;;GAYG;AACH,MAAM,OAAO,wCAAyC,SAAQ,SAAS;IAC5D,KAAK,CAAC,EAAE;QACf,4DAA4D;QAC5D,IAAI,CAAC,MAAM,CAAC,6DAA6D,CAAC,CAAC;QAC3E,IAAI,CAAC,MAAM,CAAC,8DAA8D,CAAC,CAAC;QAC5E,IAAI,CAAC,MAAM,CAAC,8DAA8D,CAAC,CAAC;QAC5E,IAAI,CAAC,MAAM,CAAC,4EAA4E,CAAC,CAAC;QAC1F,IAAI,CAAC,MAAM,CAAC,oFAAoF,CAAC,CAAC;QAClG,IAAI,CAAC,MAAM,CAAC,qEAAqE,CAAC,CAAC;QACnF,IAAI,CAAC,MAAM,CAAC,kFAAkF,CAAC,CAAC;QAChG,IAAI,CAAC,MAAM,CAAC,0DAA0D,CAAC,CAAC;QACxE,IAAI,CAAC,MAAM,CAAC,mEAAmE,CAAC,CAAC;QACjF,IAAI,CAAC,MAAM,CAAC,wEAAwE,CAAC,CAAC;QACtF,IAAI,CAAC,MAAM,CAAC,kFAAkF,CAAC,CAAC;QAChG,IAAI,CAAC,MAAM,CAAC,8EAA8E,CAAC,CAAC;QAC5F,IAAI,CAAC,MAAM,CAAC,sEAAsE,CAAC,CAAC;QACpF,IAAI,CAAC,MAAM,CAAC,oEAAoE,CAAC,CAAC;QAClF,IAAI,CAAC,MAAM,CAAC,uFAAuF,CAAC,CAAC;QAErG,8CAA8C;QAC9C,IAAI,CAAC,MAAM,CAAC;;;;;;;;;;KAUX,CAAC,CAAC;QACH,IAAI,CAAC,MAAM,CAAC,6FAA6F,CAAC,CAAC;QAC3G,IAAI,CAAC,MAAM,CACT,wKAAwK,CACzK,CAAC;QAEF,uCAAuC;QACvC,IAAI,CAAC,MAAM,CAAC;;;;;;;;;;;;;;KAcX,CAAC,CAAC;QACH,IAAI,CAAC,MAAM,CAAC,mFAAmF,CAAC,CAAC;QACjG,IAAI,CAAC,MAAM,CACT,oLAAoL,CACrL,CAAC;QAEF,0CAA0C;QAC1C,IAAI,CAAC,MAAM,CAAC;;;;;;;;;;;KAWX,CAAC,CAAC;QACH,IAAI,CAAC,MAAM,CAAC,iGAAiG,CAAC,CAAC;QAC/G,IAAI,CAAC,MAAM,CAAC,yFAAyF,CAAC,CAAC;QACvG,IAAI,CAAC,MAAM,CAAC,iFAAiF,CAAC,CAAC;QAC/F,IAAI,CAAC,MAAM,CACT,0LAA0L,CAC3L,CAAC;QACF,IAAI,CAAC,MAAM,CACT,uLAAuL,CACxL,CAAC;QAEF,kEAAkE;QAClE,IAAI,CAAC,MAAM,CAAC;;;;;;;;;;;;;;;KAeX,CAAC,CAAC;QACH,IAAI,CAAC,MAAM,CACT,8HAA8H,CAC/H,CAAC;QACF,IAAI,CAAC,MAAM,CAAC,qGAAqG,CAAC,CAAC;QACnH,IAAI,CAAC,MAAM,CAAC,iFAAiF,CAAC,CAAC;QAC/F,IAAI,CAAC,MAAM,CAAC,6FAA6F,CAAC,CAAC;QAC3G,IAAI,CAAC,MAAM,CAAC,6FAA6F,CAAC,CAAC;QAC3G,IAAI,CAAC,MAAM,CAAC,wFAAwF,CAAC,CAAC;QACtG,IAAI,CAAC,MAAM,CAAC,yFAAyF,CAAC,CAAC;QAEvG,uDAAuD;QACvD,IAAI,CAAC,MAAM,CAAC;;;;;;;;KAQX,CAAC,CAAC;QACH,IAAI,CAAC,MAAM,CACT,mIAAmI,CACpI,CAAC;QAEF,yDAAyD;QACzD,IAAI,CAAC,MAAM,CAAC;;;;;KAKX,CAAC,CAAC;IACL,CAAC;IAEQ,KAAK,CAAC,IAAI;QACjB,IAAI,CAAC,MAAM,CAAC,kDAAkD,CAAC,CAAC;QAChE,IAAI,CAAC,MAAM,CAAC,0CAA0C,CAAC,CAAC;QACxD,IAAI,CAAC,MAAM,CAAC,2CAA2C,CAAC,CAAC;QACzD,IAAI,CAAC,MAAM,CAAC,wCAAwC,CAAC,CAAC;QACtD,IAAI,CAAC,MAAM,CAAC,kFAAkF,CAAC,CAAC;QAChG,IAAI,CAAC,MAAM,CAAC,yCAAyC,CAAC,CAAC;QACvD,IAAI,CAAC,MAAM,CAAC,iDAAiD,CAAC,CAAC;QAC/D,IAAI,CAAC,MAAM,CAAC,gDAAgD,CAAC,CAAC;QAC9D,IAAI,CAAC,MAAM,CAAC,iDAAiD,CAAC,CAAC;QAC/D,IAAI,CAAC,MAAM,CAAC,qDAAqD,CAAC,CAAC;QACnE,IAAI,CAAC,MAAM,CAAC,kDAAkD,CAAC,CAAC;QAChE,IAAI,CAAC,MAAM,CAAC,sDAAsD,CAAC,CAAC;QACpE,IAAI,CAAC,MAAM,CAAC,qDAAqD,CAAC,CAAC;QACnE,IAAI,CAAC,MAAM,CAAC,uDAAuD,CAAC,CAAC;QACrE,IAAI,CAAC,MAAM,CAAC,iDAAiD,CAAC,CAAC;QAC/D,IAAI,CAAC,MAAM,CAAC,yDAAyD,CAAC,CAAC;QACvE,IAAI,CAAC,MAAM,CAAC,4DAA4D,CAAC,CAAC;QAC1E,IAAI,CAAC,MAAM,CAAC,sEAAsE,CAAC,CAAC;QACpF,IAAI,CAAC,MAAM,CAAC,kEAAkE,CAAC,CAAC;IAClF,CAAC;CACF"}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { Migration } from '@mikro-orm/migrations';
|
|
2
|
+
/**
|
|
3
|
+
* `promotion_usages.order_id` gains the foreign key it never had (D-94.1,
|
|
4
|
+
* site 3 — the family D-90 opened with `shipments.order_id`).
|
|
5
|
+
*
|
|
6
|
+
* `PromotionService.finalizeUsage(em, …)` runs on the *placement*
|
|
7
|
+
* `EntityManager`: a usage cap hit at the last moment throws and rolls the
|
|
8
|
+
* order back with it. D-78 point 2 rules such a seam permanent and *declared*,
|
|
9
|
+
* and what makes the declaration honest is a constraint. `promotion_usages`
|
|
10
|
+
* was created (`20260618T100727_promotions_engine.ts:98-116`) with a primary
|
|
11
|
+
* key, a `(order_id, promotion_id)` unique constraint and two indexes, and no
|
|
12
|
+
* foreign key on any column — so the omission tracks the module boundary, not
|
|
13
|
+
* a decision about money.
|
|
14
|
+
*
|
|
15
|
+
* `on delete restrict`: a usage row is one half of a counter. Deleting an
|
|
16
|
+
* order under `cascade` would drop the redemption while
|
|
17
|
+
* `promotion_usage_counters` keeps the increment, so the cap would silently
|
|
18
|
+
* drift. `restrict` says the only correct thing — release it first.
|
|
19
|
+
*
|
|
20
|
+
* The column accepted unconstrained values from 2026-06-18 until this
|
|
21
|
+
* migration, so the constraint is preceded by an explicit orphan report.
|
|
22
|
+
* Rows returned mean a redemption recorded against an order that is gone.
|
|
23
|
+
* Delete them — and **accept that `promotion_usage_counters` will now
|
|
24
|
+
* over-count, deliberately**. Deriving the decrement means re-deriving
|
|
25
|
+
* `buildGuards`' scope choice (`per_coupon` vs `shared_batch`, per
|
|
26
|
+
* organization, per customer) from configuration that may have changed since
|
|
27
|
+
* the redemption, in SQL, against money-adjacent state. An over-counted cap
|
|
28
|
+
* fails **closed**: a coupon retires early and an operator raises the limit.
|
|
29
|
+
* That is the safe direction, and it is recorded here so the next reader knows
|
|
30
|
+
* it was chosen rather than missed (D-94.2, remedy 2).
|
|
31
|
+
*
|
|
32
|
+
* `promotions` declares `orders` in its manifest `dependencies` for this edge
|
|
33
|
+
* (AGENTS.md § Migrations item 4). The reverse port edge — `orders` resolving
|
|
34
|
+
* `promotionService` — moves to `acknowledgedDependencies` in `orders` and in
|
|
35
|
+
* `carts`, which drops the install ordering the constraint says is backwards
|
|
36
|
+
* and keeps every refusal exactly as it was (D-94.3).
|
|
37
|
+
*/
|
|
38
|
+
export declare class Migration20260818T081251PromotionsPromotionUsageOrderFk extends Migration {
|
|
39
|
+
up(): Promise<void>;
|
|
40
|
+
down(): Promise<void>;
|
|
41
|
+
}
|
|
42
|
+
//# sourceMappingURL=20260818T081251_promotions_promotion_usage_order_fk.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"20260818T081251_promotions_promotion_usage_order_fk.d.ts","sourceRoot":"","sources":["../../src/migrations/20260818T081251_promotions_promotion_usage_order_fk.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,uBAAuB,CAAC;AAElD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmCG;AACH,qBAAa,uDAAwD,SAAQ,SAAS;IACrE,EAAE,IAAI,OAAO,CAAC,IAAI,CAAC;IAmCnB,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;CAKrC"}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { Migration } from '@mikro-orm/migrations';
|
|
2
|
+
/**
|
|
3
|
+
* `promotion_usages.order_id` gains the foreign key it never had (D-94.1,
|
|
4
|
+
* site 3 — the family D-90 opened with `shipments.order_id`).
|
|
5
|
+
*
|
|
6
|
+
* `PromotionService.finalizeUsage(em, …)` runs on the *placement*
|
|
7
|
+
* `EntityManager`: a usage cap hit at the last moment throws and rolls the
|
|
8
|
+
* order back with it. D-78 point 2 rules such a seam permanent and *declared*,
|
|
9
|
+
* and what makes the declaration honest is a constraint. `promotion_usages`
|
|
10
|
+
* was created (`20260618T100727_promotions_engine.ts:98-116`) with a primary
|
|
11
|
+
* key, a `(order_id, promotion_id)` unique constraint and two indexes, and no
|
|
12
|
+
* foreign key on any column — so the omission tracks the module boundary, not
|
|
13
|
+
* a decision about money.
|
|
14
|
+
*
|
|
15
|
+
* `on delete restrict`: a usage row is one half of a counter. Deleting an
|
|
16
|
+
* order under `cascade` would drop the redemption while
|
|
17
|
+
* `promotion_usage_counters` keeps the increment, so the cap would silently
|
|
18
|
+
* drift. `restrict` says the only correct thing — release it first.
|
|
19
|
+
*
|
|
20
|
+
* The column accepted unconstrained values from 2026-06-18 until this
|
|
21
|
+
* migration, so the constraint is preceded by an explicit orphan report.
|
|
22
|
+
* Rows returned mean a redemption recorded against an order that is gone.
|
|
23
|
+
* Delete them — and **accept that `promotion_usage_counters` will now
|
|
24
|
+
* over-count, deliberately**. Deriving the decrement means re-deriving
|
|
25
|
+
* `buildGuards`' scope choice (`per_coupon` vs `shared_batch`, per
|
|
26
|
+
* organization, per customer) from configuration that may have changed since
|
|
27
|
+
* the redemption, in SQL, against money-adjacent state. An over-counted cap
|
|
28
|
+
* fails **closed**: a coupon retires early and an operator raises the limit.
|
|
29
|
+
* That is the safe direction, and it is recorded here so the next reader knows
|
|
30
|
+
* it was chosen rather than missed (D-94.2, remedy 2).
|
|
31
|
+
*
|
|
32
|
+
* `promotions` declares `orders` in its manifest `dependencies` for this edge
|
|
33
|
+
* (AGENTS.md § Migrations item 4). The reverse port edge — `orders` resolving
|
|
34
|
+
* `promotionService` — moves to `acknowledgedDependencies` in `orders` and in
|
|
35
|
+
* `carts`, which drops the install ordering the constraint says is backwards
|
|
36
|
+
* and keeps every refusal exactly as it was (D-94.3).
|
|
37
|
+
*/
|
|
38
|
+
export class Migration20260818T081251PromotionsPromotionUsageOrderFk extends Migration {
|
|
39
|
+
async up() {
|
|
40
|
+
this.addSql(`
|
|
41
|
+
do $$
|
|
42
|
+
declare
|
|
43
|
+
orphan_count bigint;
|
|
44
|
+
sample text;
|
|
45
|
+
begin
|
|
46
|
+
select count(*) into orphan_count
|
|
47
|
+
from "promotion_usages" u
|
|
48
|
+
left join "orders" o on o."id" = u."order_id"
|
|
49
|
+
where o."id" is null;
|
|
50
|
+
|
|
51
|
+
if orphan_count > 0 then
|
|
52
|
+
select string_agg(x."order_id"::text, ', ') into sample
|
|
53
|
+
from (
|
|
54
|
+
select distinct u."order_id"
|
|
55
|
+
from "promotion_usages" u
|
|
56
|
+
left join "orders" o on o."id" = u."order_id"
|
|
57
|
+
where o."id" is null
|
|
58
|
+
limit 10
|
|
59
|
+
) x;
|
|
60
|
+
raise exception
|
|
61
|
+
'promotion_usages_order_fk cannot be added: % orphaned promotion_usages.order_id value(s) reference no orders row (first ten: %). Export and delete those redemptions; promotion_usage_counters is left over-counting on purpose, which retires a coupon early rather than letting it be redeemed past its cap. Then re-run the migration.',
|
|
62
|
+
orphan_count, sample;
|
|
63
|
+
end if;
|
|
64
|
+
end $$;
|
|
65
|
+
`);
|
|
66
|
+
this.addSql(`
|
|
67
|
+
alter table "promotion_usages"
|
|
68
|
+
add constraint "promotion_usages_order_fk" foreign key ("order_id")
|
|
69
|
+
references "orders" ("id") on delete restrict;
|
|
70
|
+
`);
|
|
71
|
+
}
|
|
72
|
+
async down() {
|
|
73
|
+
this.addSql(`alter table "promotion_usages" drop constraint if exists "promotion_usages_order_fk";`);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
//# sourceMappingURL=20260818T081251_promotions_promotion_usage_order_fk.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"20260818T081251_promotions_promotion_usage_order_fk.js","sourceRoot":"","sources":["../../src/migrations/20260818T081251_promotions_promotion_usage_order_fk.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,uBAAuB,CAAC;AAElD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmCG;AACH,MAAM,OAAO,uDAAwD,SAAQ,SAAS;IAC3E,KAAK,CAAC,EAAE;QACf,IAAI,CAAC,MAAM,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;KAyBX,CAAC,CAAC;QAEH,IAAI,CAAC,MAAM,CAAC;;;;KAIX,CAAC,CAAC;IACL,CAAC;IAEQ,KAAK,CAAC,IAAI;QACjB,IAAI,CAAC,MAAM,CACT,uFAAuF,CACxF,CAAC;IACJ,CAAC;CACF"}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { Migration } from '@mikro-orm/migrations';
|
|
2
|
+
/**
|
|
3
|
+
* The `sales_channel_promotions` sales-channel bridge — the promotions a channel resolves.
|
|
4
|
+
*
|
|
5
|
+
* It was created by the platform's frozen `Migration20260430T170044CoreSalesChannelsPromote`
|
|
6
|
+
* until `specs/120-migration-closure-bridge-ownership/` Phase 2. Under D-226 a
|
|
7
|
+
* bridge between an always-present near side (`sales_channels`, a kernel table)
|
|
8
|
+
* and a switchable far side belongs to the far side, so this module creates it:
|
|
9
|
+
* an instance that does not install `promotions` no longer has a migration corpus
|
|
10
|
+
* naming a table nothing builds.
|
|
11
|
+
*
|
|
12
|
+
* `if not exists`, because every database that has already applied the frozen
|
|
13
|
+
* migration has this table. The storage keys on the class name and holds no
|
|
14
|
+
* checksum, so nothing is re-offered there and this migration is the no-op it
|
|
15
|
+
* reads as; on a fresh database it is the creation.
|
|
16
|
+
*
|
|
17
|
+
* The statements are the frozen ones verbatim — same columns, same primary key,
|
|
18
|
+
* same two foreign keys, same index — so the two paths reach one schema. Its
|
|
19
|
+
* position needs nothing declared: `sales_channels` is the kernel's and
|
|
20
|
+
* `promotions` is created inside the frozen prefix, which every
|
|
21
|
+
* above-watermark migration runs after.
|
|
22
|
+
*/
|
|
23
|
+
export declare class Migration20260912T094715PromotionsSalesChannelPromotions extends Migration {
|
|
24
|
+
up(): Promise<void>;
|
|
25
|
+
down(): Promise<void>;
|
|
26
|
+
}
|
|
27
|
+
//# sourceMappingURL=20260912T094715_promotions_sales_channel_promotions.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"20260912T094715_promotions_sales_channel_promotions.d.ts","sourceRoot":"","sources":["../../src/migrations/20260912T094715_promotions_sales_channel_promotions.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,uBAAuB,CAAC;AAElD;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,qBAAa,wDAAyD,SAAQ,SAAS;IACtE,EAAE,IAAI,OAAO,CAAC,IAAI,CAAC;IAmBnB,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;CAGrC"}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { Migration } from '@mikro-orm/migrations';
|
|
2
|
+
/**
|
|
3
|
+
* The `sales_channel_promotions` sales-channel bridge — the promotions a channel resolves.
|
|
4
|
+
*
|
|
5
|
+
* It was created by the platform's frozen `Migration20260430T170044CoreSalesChannelsPromote`
|
|
6
|
+
* until `specs/120-migration-closure-bridge-ownership/` Phase 2. Under D-226 a
|
|
7
|
+
* bridge between an always-present near side (`sales_channels`, a kernel table)
|
|
8
|
+
* and a switchable far side belongs to the far side, so this module creates it:
|
|
9
|
+
* an instance that does not install `promotions` no longer has a migration corpus
|
|
10
|
+
* naming a table nothing builds.
|
|
11
|
+
*
|
|
12
|
+
* `if not exists`, because every database that has already applied the frozen
|
|
13
|
+
* migration has this table. The storage keys on the class name and holds no
|
|
14
|
+
* checksum, so nothing is re-offered there and this migration is the no-op it
|
|
15
|
+
* reads as; on a fresh database it is the creation.
|
|
16
|
+
*
|
|
17
|
+
* The statements are the frozen ones verbatim — same columns, same primary key,
|
|
18
|
+
* same two foreign keys, same index — so the two paths reach one schema. Its
|
|
19
|
+
* position needs nothing declared: `sales_channels` is the kernel's and
|
|
20
|
+
* `promotions` is created inside the frozen prefix, which every
|
|
21
|
+
* above-watermark migration runs after.
|
|
22
|
+
*/
|
|
23
|
+
export class Migration20260912T094715PromotionsSalesChannelPromotions extends Migration {
|
|
24
|
+
async up() {
|
|
25
|
+
this.addSql(`
|
|
26
|
+
create table if not exists "sales_channel_promotions" (
|
|
27
|
+
"sales_channel_id" uuid not null,
|
|
28
|
+
"promotion_id" uuid not null,
|
|
29
|
+
constraint "sales_channel_promotions_pkey"
|
|
30
|
+
primary key ("sales_channel_id", "promotion_id"),
|
|
31
|
+
constraint "sales_channel_promotions_channel_fk"
|
|
32
|
+
foreign key ("sales_channel_id") references "sales_channels" ("id") on delete cascade,
|
|
33
|
+
constraint "sales_channel_promotions_promotion_fk"
|
|
34
|
+
foreign key ("promotion_id") references "promotions" ("id") on delete cascade
|
|
35
|
+
);
|
|
36
|
+
`);
|
|
37
|
+
this.addSql('create index if not exists "sales_channel_promotions_promotion_id_index" ' +
|
|
38
|
+
'on "sales_channel_promotions" ("promotion_id");');
|
|
39
|
+
}
|
|
40
|
+
async down() {
|
|
41
|
+
this.addSql('drop table if exists "sales_channel_promotions" cascade;');
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
//# sourceMappingURL=20260912T094715_promotions_sales_channel_promotions.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"20260912T094715_promotions_sales_channel_promotions.js","sourceRoot":"","sources":["../../src/migrations/20260912T094715_promotions_sales_channel_promotions.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,uBAAuB,CAAC;AAElD;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,MAAM,OAAO,wDAAyD,SAAQ,SAAS;IAC5E,KAAK,CAAC,EAAE;QACf,IAAI,CAAC,MAAM,CAAC;;;;;;;;;;;KAWX,CAAC,CAAC;QACH,IAAI,CAAC,MAAM,CACT,2EAA2E;YACzE,iDAAiD,CACpD,CAAC;IACJ,CAAC;IAEQ,KAAK,CAAC,IAAI;QACjB,IAAI,CAAC,MAAM,CAAC,0DAA0D,CAAC,CAAC;IAC1E,CAAC;CACF"}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `./migrations` subpath — every migration class this module owns, as one
|
|
3
|
+
* ordered `migrations` array.
|
|
4
|
+
*
|
|
5
|
+
* The array is what the platform reads when this module is **installed**:
|
|
6
|
+
* `src/packages/package-runtime.ts` takes `exported['migrations']` and refuses
|
|
7
|
+
* the package outright when it is absent — *"the `./migrations` export of
|
|
8
|
+
* @endora-commerce/mod-promotions exports no 'migrations' array"* (D-168).
|
|
9
|
+
*
|
|
10
|
+
* **Listed in ascending timestamp, which orders this module's own migrations and
|
|
11
|
+
* nothing else** (feature 081). Where this block sits relative to
|
|
12
|
+
* every other module's is decided by the manifest `dependencies` graph:
|
|
13
|
+
* `promotions` declares `orders`, and
|
|
14
|
+
* `…T081251_…_promotion_usage_order_fk` is why — it adds
|
|
15
|
+
* `promotion_usages_order_fk` (`promotion_usages.order_id` -> `orders.id`,
|
|
16
|
+
* `on delete restrict`), so `orders`' tables must already exist when this block
|
|
17
|
+
* runs. A foreign key needs the **table**, never the owner's entity class
|
|
18
|
+
* (D-169), which is what lets that constraint stand while `./backend` publishes
|
|
19
|
+
* no entity class by name.
|
|
20
|
+
*
|
|
21
|
+
* The **named** exports stay, and the asymmetry with `./backend` — which
|
|
22
|
+
* publishes an array and no named class (D-168) — is deliberate.
|
|
23
|
+
* `db/migrations-registry.generated.ts` imports each class by name from this
|
|
24
|
+
* specifier and hands it to `migration('promotions', …)`, and a migration class
|
|
25
|
+
* name is contract in a way an entity class name is not:
|
|
26
|
+
* `mikro_orm_migrations` persists it, so it is a string every already-migrated
|
|
27
|
+
* database holds.
|
|
28
|
+
*
|
|
29
|
+
* A class that is in neither the array nor the barrel is a migration that does
|
|
30
|
+
* not run: `migration:pending` reports nothing pending and the first symptom is
|
|
31
|
+
* a query against a table nobody created.
|
|
32
|
+
*/
|
|
33
|
+
import { Migration20260505T074605PromotionsCriteria } from './20260505T074605_promotions_criteria.js';
|
|
34
|
+
import { Migration20260618T100727PromotionsEngine } from './20260618T100727_promotions_engine.js';
|
|
35
|
+
import { Migration20260818T081251PromotionsPromotionUsageOrderFk } from './20260818T081251_promotions_promotion_usage_order_fk.js';
|
|
36
|
+
import { Migration20260912T094715PromotionsSalesChannelPromotions } from './20260912T094715_promotions_sales_channel_promotions.js';
|
|
37
|
+
export declare const migrations: (typeof Migration20260505T074605PromotionsCriteria)[];
|
|
38
|
+
export { Migration20260505T074605PromotionsCriteria, Migration20260618T100727PromotionsEngine, Migration20260818T081251PromotionsPromotionUsageOrderFk, Migration20260912T094715PromotionsSalesChannelPromotions, };
|
|
39
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/migrations/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AAEH,OAAO,EAAE,0CAA0C,EAAE,MAAM,0CAA0C,CAAC;AACtG,OAAO,EAAE,wCAAwC,EAAE,MAAM,wCAAwC,CAAC;AAClG,OAAO,EAAE,uDAAuD,EAAE,MAAM,0DAA0D,CAAC;AACnI,OAAO,EAAE,wDAAwD,EAAE,MAAM,0DAA0D,CAAC;AAEpI,eAAO,MAAM,UAAU,uDAKtB,CAAC;AAEF,OAAO,EACL,0CAA0C,EAC1C,wCAAwC,EACxC,uDAAuD,EACvD,wDAAwD,GACzD,CAAC"}
|