@endora-commerce/mod-promotions 0.100.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +59 -0
- package/dist/admin/api/promotions-client.d.ts +98 -0
- package/dist/admin/api/promotions-client.d.ts.map +1 -0
- package/dist/admin/api/promotions-client.js +40 -0
- package/dist/admin/api/promotions-client.js.map +1 -0
- package/dist/admin/components/CouponGeneratorForm.d.ts +10 -0
- package/dist/admin/components/CouponGeneratorForm.d.ts.map +1 -0
- package/dist/admin/components/CouponGeneratorForm.js +56 -0
- package/dist/admin/components/CouponGeneratorForm.js.map +1 -0
- package/dist/admin/index.d.ts +43 -0
- package/dist/admin/index.d.ts.map +1 -0
- package/dist/admin/index.js +69 -0
- package/dist/admin/index.js.map +1 -0
- package/dist/admin/pages/PromotionEditPage.d.ts +9 -0
- package/dist/admin/pages/PromotionEditPage.d.ts.map +1 -0
- package/dist/admin/pages/PromotionEditPage.js +333 -0
- package/dist/admin/pages/PromotionEditPage.js.map +1 -0
- package/dist/admin/pages/PromotionRulesPage.d.ts +14 -0
- package/dist/admin/pages/PromotionRulesPage.d.ts.map +1 -0
- package/dist/admin/pages/PromotionRulesPage.js +86 -0
- package/dist/admin/pages/PromotionRulesPage.js.map +1 -0
- package/dist/admin/pages/PromotionStatsPage.d.ts +13 -0
- package/dist/admin/pages/PromotionStatsPage.d.ts.map +1 -0
- package/dist/admin/pages/PromotionStatsPage.js +44 -0
- package/dist/admin/pages/PromotionStatsPage.js.map +1 -0
- package/dist/admin/pages/PromotionsPage.d.ts +9 -0
- package/dist/admin/pages/PromotionsPage.d.ts.map +1 -0
- package/dist/admin/pages/PromotionsPage.js +79 -0
- package/dist/admin/pages/PromotionsPage.js.map +1 -0
- package/dist/backend/actions/amount-off-cart.d.ts +4 -0
- package/dist/backend/actions/amount-off-cart.d.ts.map +1 -0
- package/dist/backend/actions/amount-off-cart.js +16 -0
- package/dist/backend/actions/amount-off-cart.js.map +1 -0
- package/dist/backend/actions/buy-x-get-y-free.d.ts +8 -0
- package/dist/backend/actions/buy-x-get-y-free.d.ts.map +1 -0
- package/dist/backend/actions/buy-x-get-y-free.js +20 -0
- package/dist/backend/actions/buy-x-get-y-free.js.map +1 -0
- package/dist/backend/actions/buy-x-units-amount-off.d.ts +4 -0
- package/dist/backend/actions/buy-x-units-amount-off.d.ts.map +1 -0
- package/dist/backend/actions/buy-x-units-amount-off.js +19 -0
- package/dist/backend/actions/buy-x-units-amount-off.js.map +1 -0
- package/dist/backend/actions/buy-x-units-percent-off.d.ts +4 -0
- package/dist/backend/actions/buy-x-units-percent-off.d.ts.map +1 -0
- package/dist/backend/actions/buy-x-units-percent-off.js +15 -0
- package/dist/backend/actions/buy-x-units-percent-off.js.map +1 -0
- package/dist/backend/actions/buy-x-units-y-free.d.ts +7 -0
- package/dist/backend/actions/buy-x-units-y-free.d.ts.map +1 -0
- package/dist/backend/actions/buy-x-units-y-free.js +21 -0
- package/dist/backend/actions/buy-x-units-y-free.js.map +1 -0
- package/dist/backend/actions/every-nth-product-percent-off.d.ts +7 -0
- package/dist/backend/actions/every-nth-product-percent-off.d.ts.map +1 -0
- package/dist/backend/actions/every-nth-product-percent-off.js +17 -0
- package/dist/backend/actions/every-nth-product-percent-off.js.map +1 -0
- package/dist/backend/actions/free-delivery.d.ts +4 -0
- package/dist/backend/actions/free-delivery.d.ts.map +1 -0
- package/dist/backend/actions/free-delivery.js +11 -0
- package/dist/backend/actions/free-delivery.js.map +1 -0
- package/dist/backend/actions/percentage-off-cart.d.ts +4 -0
- package/dist/backend/actions/percentage-off-cart.d.ts.map +1 -0
- package/dist/backend/actions/percentage-off-cart.js +11 -0
- package/dist/backend/actions/percentage-off-cart.js.map +1 -0
- package/dist/backend/actions/spend-x-amount-off.d.ts +4 -0
- package/dist/backend/actions/spend-x-amount-off.d.ts.map +1 -0
- package/dist/backend/actions/spend-x-amount-off.js +17 -0
- package/dist/backend/actions/spend-x-amount-off.js.map +1 -0
- package/dist/backend/actions/spend-x-percent-off.d.ts +4 -0
- package/dist/backend/actions/spend-x-percent-off.d.ts.map +1 -0
- package/dist/backend/actions/spend-x-percent-off.js +18 -0
- package/dist/backend/actions/spend-x-percent-off.js.map +1 -0
- package/dist/backend/actions/types.d.ts +37 -0
- package/dist/backend/actions/types.d.ts.map +1 -0
- package/dist/backend/actions/types.js +2 -0
- package/dist/backend/actions/types.js.map +1 -0
- package/dist/backend/actions/util.d.ts +13 -0
- package/dist/backend/actions/util.d.ts.map +1 -0
- package/dist/backend/actions/util.js +18 -0
- package/dist/backend/actions/util.js.map +1 -0
- package/dist/backend/entities/coupon-batch.entity.d.ts +21 -0
- package/dist/backend/entities/coupon-batch.entity.d.ts.map +1 -0
- package/dist/backend/entities/coupon-batch.entity.js +78 -0
- package/dist/backend/entities/coupon-batch.entity.js.map +1 -0
- package/dist/backend/entities/promotion-coupon.entity.d.ts +17 -0
- package/dist/backend/entities/promotion-coupon.entity.d.ts.map +1 -0
- package/dist/backend/entities/promotion-coupon.entity.js +64 -0
- package/dist/backend/entities/promotion-coupon.entity.js.map +1 -0
- package/dist/backend/entities/promotion-rule.entity.d.ts +16 -0
- package/dist/backend/entities/promotion-rule.entity.d.ts.map +1 -0
- package/dist/backend/entities/promotion-rule.entity.js +57 -0
- package/dist/backend/entities/promotion-rule.entity.js.map +1 -0
- package/dist/backend/entities/promotion-usage-counter.entity.d.ts +15 -0
- package/dist/backend/entities/promotion-usage-counter.entity.d.ts.map +1 -0
- package/dist/backend/entities/promotion-usage-counter.entity.js +48 -0
- package/dist/backend/entities/promotion-usage-counter.entity.js.map +1 -0
- package/dist/backend/entities/promotion-usage.entity.d.ts +21 -0
- package/dist/backend/entities/promotion-usage.entity.d.ts.map +1 -0
- package/dist/backend/entities/promotion-usage.entity.js +89 -0
- package/dist/backend/entities/promotion-usage.entity.js.map +1 -0
- package/dist/backend/entities/promotion.entity.d.ts +63 -0
- package/dist/backend/entities/promotion.entity.d.ts.map +1 -0
- package/dist/backend/entities/promotion.entity.js +194 -0
- package/dist/backend/entities/promotion.entity.js.map +1 -0
- package/dist/backend/index.d.ts +124 -0
- package/dist/backend/index.d.ts.map +1 -0
- package/dist/backend/index.js +182 -0
- package/dist/backend/index.js.map +1 -0
- package/dist/backend/routes.d.ts +88 -0
- package/dist/backend/routes.d.ts.map +1 -0
- package/dist/backend/routes.js +256 -0
- package/dist/backend/routes.js.map +1 -0
- package/dist/backend/services/coupon-service.d.ts +40 -0
- package/dist/backend/services/coupon-service.d.ts.map +1 -0
- package/dist/backend/services/coupon-service.js +178 -0
- package/dist/backend/services/coupon-service.js.map +1 -0
- package/dist/backend/services/promotion-action-registry.d.ts +20 -0
- package/dist/backend/services/promotion-action-registry.d.ts.map +1 -0
- package/dist/backend/services/promotion-action-registry.js +57 -0
- package/dist/backend/services/promotion-action-registry.js.map +1 -0
- package/dist/backend/services/promotion-code-port.d.ts +24 -0
- package/dist/backend/services/promotion-code-port.d.ts.map +1 -0
- package/dist/backend/services/promotion-code-port.js +53 -0
- package/dist/backend/services/promotion-code-port.js.map +1 -0
- package/dist/backend/services/promotion-currency-reference.d.ts +13 -0
- package/dist/backend/services/promotion-currency-reference.d.ts.map +1 -0
- package/dist/backend/services/promotion-currency-reference.js +25 -0
- package/dist/backend/services/promotion-currency-reference.js.map +1 -0
- package/dist/backend/services/promotion-rule-evaluator.d.ts +32 -0
- package/dist/backend/services/promotion-rule-evaluator.d.ts.map +1 -0
- package/dist/backend/services/promotion-rule-evaluator.js +131 -0
- package/dist/backend/services/promotion-rule-evaluator.js.map +1 -0
- package/dist/backend/services/promotion-rule-service.d.ts +62 -0
- package/dist/backend/services/promotion-rule-service.d.ts.map +1 -0
- package/dist/backend/services/promotion-rule-service.js +95 -0
- package/dist/backend/services/promotion-rule-service.js.map +1 -0
- package/dist/backend/services/promotion-rule-store.d.ts +33 -0
- package/dist/backend/services/promotion-rule-store.d.ts.map +1 -0
- package/dist/backend/services/promotion-rule-store.js +88 -0
- package/dist/backend/services/promotion-rule-store.js.map +1 -0
- package/dist/backend/services/promotion-service.d.ts +312 -0
- package/dist/backend/services/promotion-service.d.ts.map +1 -0
- package/dist/backend/services/promotion-service.js +762 -0
- package/dist/backend/services/promotion-service.js.map +1 -0
- package/dist/backend/services/promotion-stats-service.d.ts +15 -0
- package/dist/backend/services/promotion-stats-service.d.ts.map +1 -0
- package/dist/backend/services/promotion-stats-service.js +66 -0
- package/dist/backend/services/promotion-stats-service.js.map +1 -0
- package/dist/backend/services/promotion-usage-service.d.ts +46 -0
- package/dist/backend/services/promotion-usage-service.d.ts.map +1 -0
- package/dist/backend/services/promotion-usage-service.js +168 -0
- package/dist/backend/services/promotion-usage-service.js.map +1 -0
- package/dist/manifest.d.ts +172 -0
- package/dist/manifest.d.ts.map +1 -0
- package/dist/manifest.js +140 -0
- package/dist/manifest.js.map +1 -0
- package/dist/migrations/20260505T074605_promotions_criteria.d.ts +19 -0
- package/dist/migrations/20260505T074605_promotions_criteria.d.ts.map +1 -0
- package/dist/migrations/20260505T074605_promotions_criteria.js +23 -0
- package/dist/migrations/20260505T074605_promotions_criteria.js.map +1 -0
- package/dist/migrations/20260618T100727_promotions_engine.d.ts +19 -0
- package/dist/migrations/20260618T100727_promotions_engine.d.ts.map +1 -0
- package/dist/migrations/20260618T100727_promotions_engine.js +148 -0
- package/dist/migrations/20260618T100727_promotions_engine.js.map +1 -0
- package/dist/migrations/20260818T081251_promotions_promotion_usage_order_fk.d.ts +42 -0
- package/dist/migrations/20260818T081251_promotions_promotion_usage_order_fk.d.ts.map +1 -0
- package/dist/migrations/20260818T081251_promotions_promotion_usage_order_fk.js +76 -0
- package/dist/migrations/20260818T081251_promotions_promotion_usage_order_fk.js.map +1 -0
- package/dist/migrations/20260912T094715_promotions_sales_channel_promotions.d.ts +27 -0
- package/dist/migrations/20260912T094715_promotions_sales_channel_promotions.d.ts.map +1 -0
- package/dist/migrations/20260912T094715_promotions_sales_channel_promotions.js +44 -0
- package/dist/migrations/20260912T094715_promotions_sales_channel_promotions.js.map +1 -0
- package/dist/migrations/index.d.ts +39 -0
- package/dist/migrations/index.d.ts.map +1 -0
- package/dist/migrations/index.js +44 -0
- package/dist/migrations/index.js.map +1 -0
- package/dist/ports/index.d.ts +106 -0
- package/dist/ports/index.d.ts.map +1 -0
- package/dist/ports/index.js +2 -0
- package/dist/ports/index.js.map +1 -0
- package/docs/promotions.md +96 -0
- package/i18n/en.json +34 -0
- package/i18n/pl.json +34 -0
- package/package.json +101 -0
- package/tailwind.css +14 -0
|
@@ -0,0 +1,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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"}
|