@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,312 @@
1
+ import type { EntityManager } from '@mikro-orm/postgresql';
2
+ import { type AttributeValueType, type CartSnapshot, type CatalogProductReadPort, type DictionaryValidator, type PromotionAction, type PromotionApplication, type PromotionCriterion, type PromotionRule } from '@endora-commerce/contracts';
3
+ import type { AuditPort, SalesChannelMembershipPort } from '@endora-commerce/platform/kernel';
4
+ import { Promotion } from '../entities/promotion.entity.js';
5
+ import { type PromotionActionRegistry } from './promotion-action-registry.js';
6
+ import { type FinalizeAppliedPromotion, type UsageContext } from './promotion-usage-service.js';
7
+ /**
8
+ * The one question this service asks about promotion-rule attribute metadata.
9
+ * `catalog`'s `CatalogPromoAttributePort` (feature 075, Phase P) satisfies it,
10
+ * and `backend.ts` resolves that port by its published contract type; the
11
+ * narrower shape stays here because a rule evaluation never needs the port's
12
+ * other method.
13
+ */
14
+ export interface PromotionRuleCatalogPort {
15
+ getAttributeWithOptions(key: string): Promise<{
16
+ id: string;
17
+ key: string;
18
+ valueType: AttributeValueType;
19
+ isPromoRule: boolean;
20
+ options: Array<{
21
+ value: string;
22
+ label: Record<string, string>;
23
+ labelDefault: string;
24
+ }>;
25
+ } | null>;
26
+ }
27
+ /** Audit-log sink for FR-039 skip events. Defaults to `console.info`. */
28
+ export interface PromotionAuditLogger {
29
+ info(message: string, fields?: Record<string, unknown>): void;
30
+ }
31
+ /**
32
+ * PromotionService (T132 / FR-052).
33
+ *
34
+ * `applyToCart(snapshot)` walks every active, in-window promotion and
35
+ * applies the ones the cart is eligible for, returning the adjusted
36
+ * subtotal / discountTotal / deliveryTotal / total + a per-promotion
37
+ * audit list.
38
+ *
39
+ * Application order: code-presented promotions first (so a coupon
40
+ * always trumps an automatic), then automatic ones in `value` desc.
41
+ * Discounts never push the subtotal below zero or delivery below zero.
42
+ */
43
+ export declare class PromotionService {
44
+ #private;
45
+ private readonly emFactory;
46
+ /**
47
+ * Feature 012 / US8 — `catalogPromoAttributePort`, owned by `catalog`: the
48
+ * attribute metadata a rule criterion is validated and evaluated against.
49
+ *
50
+ * **Required** (issue #164). It used to be optional "so legacy tests /
51
+ * composition setups that don't wire this stay green", and production
52
+ * always passed it — an optional dependency nobody omits is a lie in the
53
+ * type: it tells every reader that absence is supported, and it makes the
54
+ * absent branch untested by construction. Both consequences were real.
55
+ * `validateCriteria` opened by returning early, so a composition without
56
+ * the port **accepted a promotion naming an attribute that does not
57
+ * exist**; and no test covered that, because the ten call sites that
58
+ * omitted the port were the ones that never exercised attributes.
59
+ *
60
+ * There is no degrade left to express. Absence is not a state the platform
61
+ * can be in: the container resolves this port for every composition, and
62
+ * `catalog` being switched off makes the call **throw**
63
+ * `ModuleDisabledError` at the resolution seam — which is the explicit
64
+ * answer, and it is the owner's to give. What remains is
65
+ * `getAttributeWithOptions` answering `null`, already explicit in its
66
+ * return type and meaning what it says: no such attribute.
67
+ */
68
+ private readonly catalogPort;
69
+ /**
70
+ * Feature 075 Phase C — `catalogProductReadPort`, owned by `catalog`. It
71
+ * replaced an `em.find(Product, …)` this module wrote against `catalog`'s
72
+ * table to hydrate a cart line's attribute values, and it is required for
73
+ * the reason above: a composition that could not read products evaluated
74
+ * every attribute criterion against a line with no attribute values, which
75
+ * is not a degrade an operator would recognise as one.
76
+ */
77
+ private readonly productReadPort;
78
+ /**
79
+ * Feature 005 / T027b — the sanctioned bridge accessor for
80
+ * `sales_channel_promotions`: it auto-binds a newly-created Promotion to
81
+ * the system-default channel, and it answers which promotions a channel
82
+ * holds when a cart is evaluated.
83
+ *
84
+ * **Required** (issue #251), and of the four this is the one whose absent
85
+ * form was a Principle XII hole rather than a quiet one. Omitting it did
86
+ * two things at once: a new promotion bound to **no** channel, and
87
+ * `applyToCart` skipped channel filtering altogether — so a promotion an
88
+ * operator scoped to one channel applied in every one of them. Both roots
89
+ * passed the service, so nothing was live; every service built by
90
+ * `test/helpers/promotion-service.ts` was not, which is ten suites
91
+ * asserting against a promotion engine with no channel scope.
92
+ *
93
+ * There is no degrade to express in a return type. `sales_channels` is
94
+ * `nonDeactivatable` and `salesChannelMembershipPort` is a
95
+ * platform-owned container name besides, so absence is not a state a
96
+ * composition can reach.
97
+ */
98
+ private readonly salesChannelMembership;
99
+ /**
100
+ * Feature 037 — the dictionary reference validator, owned by
101
+ * `dictionaries`: it is what makes a promotion's `currency` a code the
102
+ * platform actually knows.
103
+ *
104
+ * **Required** (issue #251). `validateCurrency` opened with "no validator,
105
+ * no validation" and returned, so a composition without it stored a
106
+ * promotion denominated in a currency that does not exist — and the
107
+ * failure surfaced far downstream, at the cart, as a discount in a
108
+ * currency nothing could price. `dictionaries` is a declared dependency
109
+ * and is `nonDeactivatable`, so absence is not a state a composition can
110
+ * reach; an unknown or withdrawn code is already explicit, as the 409 this
111
+ * method raises.
112
+ */
113
+ private readonly dictionaryValidator;
114
+ /**
115
+ * Feature 026 US5 — the Organization status gate: an org-targeted
116
+ * promotion only applies while its Organization is `active`.
117
+ *
118
+ * **Required** (issue #251), and the one `backend.ts` already named in its
119
+ * own comment as *"a gate whose absent form is open"*. Omitted, the check
120
+ * was skipped entirely, so a `blocked` (formerly `suspended`) or
121
+ * `pending_verification` customer kept collecting its negotiated
122
+ * discounts — the exact thing US5 exists to stop.
123
+ *
124
+ * The degrade that **is** real stays in the return type, the way
125
+ * `allowedIdsFor(): Promise<string[] | null>` does: `null` means the
126
+ * Organization could not be read (missing or soft-deleted), and a status
127
+ * that is not `active` — `null` included — skips the org-targeted
128
+ * promotion. That is a closed answer, and it is the only absence this
129
+ * argument has.
130
+ */
131
+ private readonly resolveOrganizationStatus;
132
+ /**
133
+ * Feature 054 — co-transactional audit sink (`audit_log_entries`),
134
+ * Principle XIII.
135
+ *
136
+ * **Required** (issue #251). Without it every promotion create, update and
137
+ * delete committed with no audit row, which is not a degrade — it is the
138
+ * write happening unrecorded. `auditLogService` is a platform-owned
139
+ * container name that every composition supplies, so there was never a
140
+ * composition the absent branch described.
141
+ */
142
+ private readonly auditLog;
143
+ /** Feature 012 / US8 — audit sink for FR-039 skip-on-toggle events. */
144
+ private readonly auditLogger;
145
+ /** Feature 045 — pluggable action catalogue. Defaults to the built-ins. */
146
+ private readonly actionRegistry;
147
+ constructor(emFactory: () => EntityManager,
148
+ /**
149
+ * Feature 012 / US8 — `catalogPromoAttributePort`, owned by `catalog`: the
150
+ * attribute metadata a rule criterion is validated and evaluated against.
151
+ *
152
+ * **Required** (issue #164). It used to be optional "so legacy tests /
153
+ * composition setups that don't wire this stay green", and production
154
+ * always passed it — an optional dependency nobody omits is a lie in the
155
+ * type: it tells every reader that absence is supported, and it makes the
156
+ * absent branch untested by construction. Both consequences were real.
157
+ * `validateCriteria` opened by returning early, so a composition without
158
+ * the port **accepted a promotion naming an attribute that does not
159
+ * exist**; and no test covered that, because the ten call sites that
160
+ * omitted the port were the ones that never exercised attributes.
161
+ *
162
+ * There is no degrade left to express. Absence is not a state the platform
163
+ * can be in: the container resolves this port for every composition, and
164
+ * `catalog` being switched off makes the call **throw**
165
+ * `ModuleDisabledError` at the resolution seam — which is the explicit
166
+ * answer, and it is the owner's to give. What remains is
167
+ * `getAttributeWithOptions` answering `null`, already explicit in its
168
+ * return type and meaning what it says: no such attribute.
169
+ */
170
+ catalogPort: PromotionRuleCatalogPort,
171
+ /**
172
+ * Feature 075 Phase C — `catalogProductReadPort`, owned by `catalog`. It
173
+ * replaced an `em.find(Product, …)` this module wrote against `catalog`'s
174
+ * table to hydrate a cart line's attribute values, and it is required for
175
+ * the reason above: a composition that could not read products evaluated
176
+ * every attribute criterion against a line with no attribute values, which
177
+ * is not a degrade an operator would recognise as one.
178
+ */
179
+ productReadPort: CatalogProductReadPort,
180
+ /**
181
+ * Feature 005 / T027b — the sanctioned bridge accessor for
182
+ * `sales_channel_promotions`: it auto-binds a newly-created Promotion to
183
+ * the system-default channel, and it answers which promotions a channel
184
+ * holds when a cart is evaluated.
185
+ *
186
+ * **Required** (issue #251), and of the four this is the one whose absent
187
+ * form was a Principle XII hole rather than a quiet one. Omitting it did
188
+ * two things at once: a new promotion bound to **no** channel, and
189
+ * `applyToCart` skipped channel filtering altogether — so a promotion an
190
+ * operator scoped to one channel applied in every one of them. Both roots
191
+ * passed the service, so nothing was live; every service built by
192
+ * `test/helpers/promotion-service.ts` was not, which is ten suites
193
+ * asserting against a promotion engine with no channel scope.
194
+ *
195
+ * There is no degrade to express in a return type. `sales_channels` is
196
+ * `nonDeactivatable` and `salesChannelMembershipPort` is a
197
+ * platform-owned container name besides, so absence is not a state a
198
+ * composition can reach.
199
+ */
200
+ salesChannelMembership: SalesChannelMembershipPort,
201
+ /**
202
+ * Feature 037 — the dictionary reference validator, owned by
203
+ * `dictionaries`: it is what makes a promotion's `currency` a code the
204
+ * platform actually knows.
205
+ *
206
+ * **Required** (issue #251). `validateCurrency` opened with "no validator,
207
+ * no validation" and returned, so a composition without it stored a
208
+ * promotion denominated in a currency that does not exist — and the
209
+ * failure surfaced far downstream, at the cart, as a discount in a
210
+ * currency nothing could price. `dictionaries` is a declared dependency
211
+ * and is `nonDeactivatable`, so absence is not a state a composition can
212
+ * reach; an unknown or withdrawn code is already explicit, as the 409 this
213
+ * method raises.
214
+ */
215
+ dictionaryValidator: DictionaryValidator,
216
+ /**
217
+ * Feature 026 US5 — the Organization status gate: an org-targeted
218
+ * promotion only applies while its Organization is `active`.
219
+ *
220
+ * **Required** (issue #251), and the one `backend.ts` already named in its
221
+ * own comment as *"a gate whose absent form is open"*. Omitted, the check
222
+ * was skipped entirely, so a `blocked` (formerly `suspended`) or
223
+ * `pending_verification` customer kept collecting its negotiated
224
+ * discounts — the exact thing US5 exists to stop.
225
+ *
226
+ * The degrade that **is** real stays in the return type, the way
227
+ * `allowedIdsFor(): Promise<string[] | null>` does: `null` means the
228
+ * Organization could not be read (missing or soft-deleted), and a status
229
+ * that is not `active` — `null` included — skips the org-targeted
230
+ * promotion. That is a closed answer, and it is the only absence this
231
+ * argument has.
232
+ */
233
+ resolveOrganizationStatus: (orgId: string) => Promise<string | null>,
234
+ /**
235
+ * Feature 054 — co-transactional audit sink (`audit_log_entries`),
236
+ * Principle XIII.
237
+ *
238
+ * **Required** (issue #251). Without it every promotion create, update and
239
+ * delete committed with no audit row, which is not a degrade — it is the
240
+ * write happening unrecorded. `auditLogService` is a platform-owned
241
+ * container name that every composition supplies, so there was never a
242
+ * composition the absent branch described.
243
+ */
244
+ auditLog: AuditPort,
245
+ /** Feature 012 / US8 — audit sink for FR-039 skip-on-toggle events. */
246
+ auditLogger?: PromotionAuditLogger,
247
+ /** Feature 045 — pluggable action catalogue. Defaults to the built-ins. */
248
+ actionRegistry?: PromotionActionRegistry);
249
+ private readonly usageService;
250
+ /**
251
+ * Feature 045 (US5) — finalize usage for a placed order. MUST run inside the
252
+ * order-placement transaction (the passed `em` is that tx). Throws 409 if a
253
+ * usage cap is hit at the last moment so the transaction rolls back.
254
+ */
255
+ finalizeUsage(em: EntityManager, input: {
256
+ orderId: string;
257
+ currency: string;
258
+ ctx: UsageContext;
259
+ applied: FinalizeAppliedPromotion[];
260
+ }): Promise<void>;
261
+ /** Expose the action catalogue for the admin `action-types` endpoint. */
262
+ listActionTypes(): Array<{
263
+ type: string;
264
+ labelKey: string;
265
+ }>;
266
+ list(): Promise<Promotion[]>;
267
+ getById(id: string): Promise<Promotion>;
268
+ upsert(input: UpsertPromotionInput): Promise<Promotion>;
269
+ /** Feature 045 — update an existing promotion by id (admin edit path). */
270
+ updateById(id: string, input: UpsertPromotionInput): Promise<Promotion>;
271
+ private validateUpsertInput;
272
+ remove(id: string): Promise<void>;
273
+ private validateCurrency;
274
+ applyToCart(snapshot: CartSnapshot): Promise<PromotionApplication>;
275
+ /**
276
+ * Feature 012 / US8 — validate a criteria array against the catalog
277
+ * port. Throws `HttpError(400, ...)` for the documented error codes
278
+ * (`attribute_not_found`, `attribute_not_promo_eligible`,
279
+ * `invalid_criterion_op`, `invalid_criterion_values`,
280
+ * `invalid_option_value`).
281
+ */
282
+ private validateCriteria;
283
+ private buildAttributeLookup;
284
+ private hydrateLineAttributeValues;
285
+ private computeLineBase;
286
+ }
287
+ export interface UpsertPromotionInput {
288
+ code?: string | null;
289
+ name: string;
290
+ kind?: 'percentage_off' | 'amount_off' | 'free_delivery';
291
+ value?: number;
292
+ currency?: string | null;
293
+ minCartSubtotal?: number | null;
294
+ validFrom?: string | null;
295
+ validUntil?: string | null;
296
+ organizationId?: string | null;
297
+ customerGroupId?: string | null;
298
+ categoryId?: string | null;
299
+ productId?: string | null;
300
+ criteria?: PromotionCriterion[];
301
+ isActive?: boolean;
302
+ description?: string | null;
303
+ priority?: number;
304
+ stopFurther?: boolean;
305
+ action?: PromotionAction;
306
+ ruleId?: string | null;
307
+ rule?: PromotionRule | null;
308
+ usageLimitGlobal?: number | null;
309
+ usageLimitPerOrganization?: number | null;
310
+ usageLimitPerCustomer?: number | null;
311
+ }
312
+ //# sourceMappingURL=promotion-service.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"promotion-service.d.ts","sourceRoot":"","sources":["../../../src/backend/services/promotion-service.ts"],"names":[],"mappings":"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"}