@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.
Files changed (183) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +59 -0
  3. package/dist/admin/api/promotions-client.d.ts +98 -0
  4. package/dist/admin/api/promotions-client.d.ts.map +1 -0
  5. package/dist/admin/api/promotions-client.js +40 -0
  6. package/dist/admin/api/promotions-client.js.map +1 -0
  7. package/dist/admin/components/CouponGeneratorForm.d.ts +10 -0
  8. package/dist/admin/components/CouponGeneratorForm.d.ts.map +1 -0
  9. package/dist/admin/components/CouponGeneratorForm.js +56 -0
  10. package/dist/admin/components/CouponGeneratorForm.js.map +1 -0
  11. package/dist/admin/index.d.ts +43 -0
  12. package/dist/admin/index.d.ts.map +1 -0
  13. package/dist/admin/index.js +69 -0
  14. package/dist/admin/index.js.map +1 -0
  15. package/dist/admin/pages/PromotionEditPage.d.ts +9 -0
  16. package/dist/admin/pages/PromotionEditPage.d.ts.map +1 -0
  17. package/dist/admin/pages/PromotionEditPage.js +333 -0
  18. package/dist/admin/pages/PromotionEditPage.js.map +1 -0
  19. package/dist/admin/pages/PromotionRulesPage.d.ts +14 -0
  20. package/dist/admin/pages/PromotionRulesPage.d.ts.map +1 -0
  21. package/dist/admin/pages/PromotionRulesPage.js +86 -0
  22. package/dist/admin/pages/PromotionRulesPage.js.map +1 -0
  23. package/dist/admin/pages/PromotionStatsPage.d.ts +13 -0
  24. package/dist/admin/pages/PromotionStatsPage.d.ts.map +1 -0
  25. package/dist/admin/pages/PromotionStatsPage.js +44 -0
  26. package/dist/admin/pages/PromotionStatsPage.js.map +1 -0
  27. package/dist/admin/pages/PromotionsPage.d.ts +9 -0
  28. package/dist/admin/pages/PromotionsPage.d.ts.map +1 -0
  29. package/dist/admin/pages/PromotionsPage.js +79 -0
  30. package/dist/admin/pages/PromotionsPage.js.map +1 -0
  31. package/dist/backend/actions/amount-off-cart.d.ts +4 -0
  32. package/dist/backend/actions/amount-off-cart.d.ts.map +1 -0
  33. package/dist/backend/actions/amount-off-cart.js +16 -0
  34. package/dist/backend/actions/amount-off-cart.js.map +1 -0
  35. package/dist/backend/actions/buy-x-get-y-free.d.ts +8 -0
  36. package/dist/backend/actions/buy-x-get-y-free.d.ts.map +1 -0
  37. package/dist/backend/actions/buy-x-get-y-free.js +20 -0
  38. package/dist/backend/actions/buy-x-get-y-free.js.map +1 -0
  39. package/dist/backend/actions/buy-x-units-amount-off.d.ts +4 -0
  40. package/dist/backend/actions/buy-x-units-amount-off.d.ts.map +1 -0
  41. package/dist/backend/actions/buy-x-units-amount-off.js +19 -0
  42. package/dist/backend/actions/buy-x-units-amount-off.js.map +1 -0
  43. package/dist/backend/actions/buy-x-units-percent-off.d.ts +4 -0
  44. package/dist/backend/actions/buy-x-units-percent-off.d.ts.map +1 -0
  45. package/dist/backend/actions/buy-x-units-percent-off.js +15 -0
  46. package/dist/backend/actions/buy-x-units-percent-off.js.map +1 -0
  47. package/dist/backend/actions/buy-x-units-y-free.d.ts +7 -0
  48. package/dist/backend/actions/buy-x-units-y-free.d.ts.map +1 -0
  49. package/dist/backend/actions/buy-x-units-y-free.js +21 -0
  50. package/dist/backend/actions/buy-x-units-y-free.js.map +1 -0
  51. package/dist/backend/actions/every-nth-product-percent-off.d.ts +7 -0
  52. package/dist/backend/actions/every-nth-product-percent-off.d.ts.map +1 -0
  53. package/dist/backend/actions/every-nth-product-percent-off.js +17 -0
  54. package/dist/backend/actions/every-nth-product-percent-off.js.map +1 -0
  55. package/dist/backend/actions/free-delivery.d.ts +4 -0
  56. package/dist/backend/actions/free-delivery.d.ts.map +1 -0
  57. package/dist/backend/actions/free-delivery.js +11 -0
  58. package/dist/backend/actions/free-delivery.js.map +1 -0
  59. package/dist/backend/actions/percentage-off-cart.d.ts +4 -0
  60. package/dist/backend/actions/percentage-off-cart.d.ts.map +1 -0
  61. package/dist/backend/actions/percentage-off-cart.js +11 -0
  62. package/dist/backend/actions/percentage-off-cart.js.map +1 -0
  63. package/dist/backend/actions/spend-x-amount-off.d.ts +4 -0
  64. package/dist/backend/actions/spend-x-amount-off.d.ts.map +1 -0
  65. package/dist/backend/actions/spend-x-amount-off.js +17 -0
  66. package/dist/backend/actions/spend-x-amount-off.js.map +1 -0
  67. package/dist/backend/actions/spend-x-percent-off.d.ts +4 -0
  68. package/dist/backend/actions/spend-x-percent-off.d.ts.map +1 -0
  69. package/dist/backend/actions/spend-x-percent-off.js +18 -0
  70. package/dist/backend/actions/spend-x-percent-off.js.map +1 -0
  71. package/dist/backend/actions/types.d.ts +37 -0
  72. package/dist/backend/actions/types.d.ts.map +1 -0
  73. package/dist/backend/actions/types.js +2 -0
  74. package/dist/backend/actions/types.js.map +1 -0
  75. package/dist/backend/actions/util.d.ts +13 -0
  76. package/dist/backend/actions/util.d.ts.map +1 -0
  77. package/dist/backend/actions/util.js +18 -0
  78. package/dist/backend/actions/util.js.map +1 -0
  79. package/dist/backend/entities/coupon-batch.entity.d.ts +21 -0
  80. package/dist/backend/entities/coupon-batch.entity.d.ts.map +1 -0
  81. package/dist/backend/entities/coupon-batch.entity.js +78 -0
  82. package/dist/backend/entities/coupon-batch.entity.js.map +1 -0
  83. package/dist/backend/entities/promotion-coupon.entity.d.ts +17 -0
  84. package/dist/backend/entities/promotion-coupon.entity.d.ts.map +1 -0
  85. package/dist/backend/entities/promotion-coupon.entity.js +64 -0
  86. package/dist/backend/entities/promotion-coupon.entity.js.map +1 -0
  87. package/dist/backend/entities/promotion-rule.entity.d.ts +16 -0
  88. package/dist/backend/entities/promotion-rule.entity.d.ts.map +1 -0
  89. package/dist/backend/entities/promotion-rule.entity.js +57 -0
  90. package/dist/backend/entities/promotion-rule.entity.js.map +1 -0
  91. package/dist/backend/entities/promotion-usage-counter.entity.d.ts +15 -0
  92. package/dist/backend/entities/promotion-usage-counter.entity.d.ts.map +1 -0
  93. package/dist/backend/entities/promotion-usage-counter.entity.js +48 -0
  94. package/dist/backend/entities/promotion-usage-counter.entity.js.map +1 -0
  95. package/dist/backend/entities/promotion-usage.entity.d.ts +21 -0
  96. package/dist/backend/entities/promotion-usage.entity.d.ts.map +1 -0
  97. package/dist/backend/entities/promotion-usage.entity.js +89 -0
  98. package/dist/backend/entities/promotion-usage.entity.js.map +1 -0
  99. package/dist/backend/entities/promotion.entity.d.ts +63 -0
  100. package/dist/backend/entities/promotion.entity.d.ts.map +1 -0
  101. package/dist/backend/entities/promotion.entity.js +194 -0
  102. package/dist/backend/entities/promotion.entity.js.map +1 -0
  103. package/dist/backend/index.d.ts +124 -0
  104. package/dist/backend/index.d.ts.map +1 -0
  105. package/dist/backend/index.js +182 -0
  106. package/dist/backend/index.js.map +1 -0
  107. package/dist/backend/routes.d.ts +88 -0
  108. package/dist/backend/routes.d.ts.map +1 -0
  109. package/dist/backend/routes.js +256 -0
  110. package/dist/backend/routes.js.map +1 -0
  111. package/dist/backend/services/coupon-service.d.ts +40 -0
  112. package/dist/backend/services/coupon-service.d.ts.map +1 -0
  113. package/dist/backend/services/coupon-service.js +178 -0
  114. package/dist/backend/services/coupon-service.js.map +1 -0
  115. package/dist/backend/services/promotion-action-registry.d.ts +20 -0
  116. package/dist/backend/services/promotion-action-registry.d.ts.map +1 -0
  117. package/dist/backend/services/promotion-action-registry.js +57 -0
  118. package/dist/backend/services/promotion-action-registry.js.map +1 -0
  119. package/dist/backend/services/promotion-code-port.d.ts +24 -0
  120. package/dist/backend/services/promotion-code-port.d.ts.map +1 -0
  121. package/dist/backend/services/promotion-code-port.js +53 -0
  122. package/dist/backend/services/promotion-code-port.js.map +1 -0
  123. package/dist/backend/services/promotion-currency-reference.d.ts +13 -0
  124. package/dist/backend/services/promotion-currency-reference.d.ts.map +1 -0
  125. package/dist/backend/services/promotion-currency-reference.js +25 -0
  126. package/dist/backend/services/promotion-currency-reference.js.map +1 -0
  127. package/dist/backend/services/promotion-rule-evaluator.d.ts +32 -0
  128. package/dist/backend/services/promotion-rule-evaluator.d.ts.map +1 -0
  129. package/dist/backend/services/promotion-rule-evaluator.js +131 -0
  130. package/dist/backend/services/promotion-rule-evaluator.js.map +1 -0
  131. package/dist/backend/services/promotion-rule-service.d.ts +62 -0
  132. package/dist/backend/services/promotion-rule-service.d.ts.map +1 -0
  133. package/dist/backend/services/promotion-rule-service.js +95 -0
  134. package/dist/backend/services/promotion-rule-service.js.map +1 -0
  135. package/dist/backend/services/promotion-rule-store.d.ts +33 -0
  136. package/dist/backend/services/promotion-rule-store.d.ts.map +1 -0
  137. package/dist/backend/services/promotion-rule-store.js +88 -0
  138. package/dist/backend/services/promotion-rule-store.js.map +1 -0
  139. package/dist/backend/services/promotion-service.d.ts +312 -0
  140. package/dist/backend/services/promotion-service.d.ts.map +1 -0
  141. package/dist/backend/services/promotion-service.js +762 -0
  142. package/dist/backend/services/promotion-service.js.map +1 -0
  143. package/dist/backend/services/promotion-stats-service.d.ts +15 -0
  144. package/dist/backend/services/promotion-stats-service.d.ts.map +1 -0
  145. package/dist/backend/services/promotion-stats-service.js +66 -0
  146. package/dist/backend/services/promotion-stats-service.js.map +1 -0
  147. package/dist/backend/services/promotion-usage-service.d.ts +46 -0
  148. package/dist/backend/services/promotion-usage-service.d.ts.map +1 -0
  149. package/dist/backend/services/promotion-usage-service.js +168 -0
  150. package/dist/backend/services/promotion-usage-service.js.map +1 -0
  151. package/dist/manifest.d.ts +172 -0
  152. package/dist/manifest.d.ts.map +1 -0
  153. package/dist/manifest.js +140 -0
  154. package/dist/manifest.js.map +1 -0
  155. package/dist/migrations/20260505T074605_promotions_criteria.d.ts +19 -0
  156. package/dist/migrations/20260505T074605_promotions_criteria.d.ts.map +1 -0
  157. package/dist/migrations/20260505T074605_promotions_criteria.js +23 -0
  158. package/dist/migrations/20260505T074605_promotions_criteria.js.map +1 -0
  159. package/dist/migrations/20260618T100727_promotions_engine.d.ts +19 -0
  160. package/dist/migrations/20260618T100727_promotions_engine.d.ts.map +1 -0
  161. package/dist/migrations/20260618T100727_promotions_engine.js +148 -0
  162. package/dist/migrations/20260618T100727_promotions_engine.js.map +1 -0
  163. package/dist/migrations/20260818T081251_promotions_promotion_usage_order_fk.d.ts +42 -0
  164. package/dist/migrations/20260818T081251_promotions_promotion_usage_order_fk.d.ts.map +1 -0
  165. package/dist/migrations/20260818T081251_promotions_promotion_usage_order_fk.js +76 -0
  166. package/dist/migrations/20260818T081251_promotions_promotion_usage_order_fk.js.map +1 -0
  167. package/dist/migrations/20260912T094715_promotions_sales_channel_promotions.d.ts +27 -0
  168. package/dist/migrations/20260912T094715_promotions_sales_channel_promotions.d.ts.map +1 -0
  169. package/dist/migrations/20260912T094715_promotions_sales_channel_promotions.js +44 -0
  170. package/dist/migrations/20260912T094715_promotions_sales_channel_promotions.js.map +1 -0
  171. package/dist/migrations/index.d.ts +39 -0
  172. package/dist/migrations/index.d.ts.map +1 -0
  173. package/dist/migrations/index.js +44 -0
  174. package/dist/migrations/index.js.map +1 -0
  175. package/dist/ports/index.d.ts +106 -0
  176. package/dist/ports/index.d.ts.map +1 -0
  177. package/dist/ports/index.js +2 -0
  178. package/dist/ports/index.js.map +1 -0
  179. package/docs/promotions.md +96 -0
  180. package/i18n/en.json +34 -0
  181. package/i18n/pl.json +34 -0
  182. package/package.json +101 -0
  183. package/tailwind.css +14 -0
@@ -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":"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"}
@@ -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"}