@classytic/promo 0.1.0 → 0.2.1
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/CHANGELOG.md +128 -0
- package/README.md +226 -22
- package/dist/{index-J5BC20DN.d.mts → constants-CrbSSQG5.d.mts} +1 -3
- package/dist/{constants-BVajdyL3.mjs → constants-D0Rntp2f.mjs} +1 -3
- package/dist/index.d.mts +1301 -10
- package/dist/index.mjs +2165 -46
- package/dist/schemas/index.d.mts +253 -0
- package/dist/schemas/index.mjs +134 -0
- package/package.json +23 -37
- package/dist/config-iZjn_8pp.d.mts +0 -71
- package/dist/domain/enums/index.d.mts +0 -2
- package/dist/domain/enums/index.mjs +0 -2
- package/dist/domain/index.d.mts +0 -61
- package/dist/domain/index.mjs +0 -4
- package/dist/domain-errors-BEkXvy5O.mjs +0 -80
- package/dist/event-emitter.port-DaodlJSG.d.mts +0 -8
- package/dist/event-types-CsTV1FKX.mjs +0 -25
- package/dist/events/index.d.mts +0 -2
- package/dist/events/index.mjs +0 -3
- package/dist/events-CprEWlN7.mjs +0 -25
- package/dist/index-B7lLH19a.d.mts +0 -13
- package/dist/index-C52zSBkI.d.mts +0 -96
- package/dist/index-Cu9iwy4v.d.mts +0 -99
- package/dist/index-l09KqnlE.d.mts +0 -81
- package/dist/models/index.d.mts +0 -2
- package/dist/models/index.mjs +0 -2
- package/dist/models-DdBNae7h.mjs +0 -277
- package/dist/repositories/index.d.mts +0 -2
- package/dist/repositories/index.mjs +0 -2
- package/dist/repositories-DgZIY9wD.mjs +0 -295
- package/dist/results-Ca5ZCNbN.d.mts +0 -218
- package/dist/services/index.d.mts +0 -2
- package/dist/services/index.mjs +0 -2
- package/dist/services-Cz0gHrmX.mjs +0 -815
- package/dist/types/index.d.mts +0 -3
- package/dist/types/index.mjs +0 -1
- package/dist/unit-of-work.port-DaMW8WZK.d.mts +0 -7
- package/dist/voucher.port-yxfb3MHJ.d.mts +0 -146
package/dist/index.d.mts
CHANGED
|
@@ -1,20 +1,1311 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
1
|
+
import { c as ProgramType, f as StackingMode, g as VoucherStatus, i as DiscountScope, m as TriggerMode, r as DiscountMode, s as ProgramStatus, u as RewardType } from "./constants-CrbSSQG5.mjs";
|
|
2
|
+
import { PluginType as PluginType$1, Repository } from "@classytic/mongokit";
|
|
3
|
+
import { ResolvedTenantConfig, TenantConfig, TenantFieldType } from "@classytic/primitives/tenant";
|
|
4
|
+
import { DomainEvent, DomainEvent as DomainEvent$1, EventHandler, EventTransport, EventTransport as EventTransport$1 } from "@classytic/primitives/events";
|
|
5
|
+
import { ClientSession, Connection, Model } from "mongoose";
|
|
6
|
+
import { z } from "zod";
|
|
7
|
+
import { OutboxStore } from "@classytic/primitives/outbox";
|
|
8
|
+
import { OperationContext } from "@classytic/primitives/context";
|
|
9
9
|
|
|
10
|
+
//#region src/types/inputs.d.ts
|
|
11
|
+
/**
|
|
12
|
+
* Extends `@classytic/primitives`' {@link OperationContext}. The
|
|
13
|
+
* `[key: string]: unknown` fall-through lets hosts pass dynamic tenant keys
|
|
14
|
+
* when they've configured a custom `contextKey` on `TenantConfig`.
|
|
15
|
+
*/
|
|
16
|
+
interface PromoContext extends OperationContext {
|
|
17
|
+
/** Narrowed from primitives' `IdLike` to string. */
|
|
18
|
+
actorId?: string;
|
|
19
|
+
/** Narrowed from primitives' `IdLike` to string. */
|
|
20
|
+
organizationId?: string;
|
|
21
|
+
/** Narrows primitives' `session: unknown` to the concrete Mongoose type so
|
|
22
|
+
* this context flows directly into mongokit repository calls. */
|
|
23
|
+
session?: ClientSession;
|
|
24
|
+
[key: string]: unknown;
|
|
25
|
+
}
|
|
26
|
+
interface CreateProgramInput {
|
|
27
|
+
name: string;
|
|
28
|
+
description?: string;
|
|
29
|
+
programType: ProgramType;
|
|
30
|
+
triggerMode: TriggerMode;
|
|
31
|
+
stackingMode?: StackingMode;
|
|
32
|
+
priority?: number;
|
|
33
|
+
startsAt?: Date;
|
|
34
|
+
endsAt?: Date;
|
|
35
|
+
maxUsageTotal?: number;
|
|
36
|
+
maxUsagePerCustomer?: number;
|
|
37
|
+
applicableCustomerIds?: string[];
|
|
38
|
+
applicableCustomerTags?: string[];
|
|
39
|
+
metadata?: Record<string, unknown>;
|
|
40
|
+
}
|
|
41
|
+
interface UpdateProgramInput {
|
|
42
|
+
name?: string;
|
|
43
|
+
description?: string;
|
|
44
|
+
stackingMode?: StackingMode;
|
|
45
|
+
priority?: number;
|
|
46
|
+
startsAt?: Date;
|
|
47
|
+
endsAt?: Date;
|
|
48
|
+
maxUsageTotal?: number;
|
|
49
|
+
maxUsagePerCustomer?: number;
|
|
50
|
+
applicableCustomerIds?: string[];
|
|
51
|
+
applicableCustomerTags?: string[];
|
|
52
|
+
metadata?: Record<string, unknown>;
|
|
53
|
+
}
|
|
54
|
+
interface CreateRuleInput {
|
|
55
|
+
name?: string;
|
|
56
|
+
minimumAmount?: number;
|
|
57
|
+
minimumQuantity?: number;
|
|
58
|
+
applicableProductIds?: string[];
|
|
59
|
+
applicableCategories?: string[];
|
|
60
|
+
applicableSkus?: string[];
|
|
61
|
+
buyQuantity?: number;
|
|
62
|
+
code?: string;
|
|
63
|
+
startsAt?: Date;
|
|
64
|
+
endsAt?: Date;
|
|
65
|
+
metadata?: Record<string, unknown>;
|
|
66
|
+
}
|
|
67
|
+
interface UpdateRuleInput {
|
|
68
|
+
name?: string;
|
|
69
|
+
minimumAmount?: number;
|
|
70
|
+
minimumQuantity?: number;
|
|
71
|
+
applicableProductIds?: string[];
|
|
72
|
+
applicableCategories?: string[];
|
|
73
|
+
applicableSkus?: string[];
|
|
74
|
+
buyQuantity?: number;
|
|
75
|
+
code?: string;
|
|
76
|
+
startsAt?: Date;
|
|
77
|
+
endsAt?: Date;
|
|
78
|
+
metadata?: Record<string, unknown>;
|
|
79
|
+
}
|
|
80
|
+
interface CreateRewardInput {
|
|
81
|
+
ruleId?: string;
|
|
82
|
+
rewardType: RewardType;
|
|
83
|
+
discountMode?: DiscountMode;
|
|
84
|
+
discountAmount?: number;
|
|
85
|
+
maxDiscountAmount?: number;
|
|
86
|
+
discountScope?: DiscountScope;
|
|
87
|
+
applicableProductIds?: string[];
|
|
88
|
+
freeProductId?: string;
|
|
89
|
+
freeProductSku?: string;
|
|
90
|
+
freeQuantity?: number;
|
|
91
|
+
giftCardAmount?: number;
|
|
92
|
+
metadata?: Record<string, unknown>;
|
|
93
|
+
}
|
|
94
|
+
interface UpdateRewardInput {
|
|
95
|
+
discountMode?: DiscountMode;
|
|
96
|
+
discountAmount?: number;
|
|
97
|
+
maxDiscountAmount?: number;
|
|
98
|
+
discountScope?: DiscountScope;
|
|
99
|
+
applicableProductIds?: string[];
|
|
100
|
+
freeProductId?: string;
|
|
101
|
+
freeProductSku?: string;
|
|
102
|
+
freeQuantity?: number;
|
|
103
|
+
giftCardAmount?: number;
|
|
104
|
+
metadata?: Record<string, unknown>;
|
|
105
|
+
}
|
|
106
|
+
interface GenerateCodesInput {
|
|
107
|
+
programId: string;
|
|
108
|
+
count: number;
|
|
109
|
+
customerId?: string;
|
|
110
|
+
expiresAt?: Date;
|
|
111
|
+
metadata?: Record<string, unknown>;
|
|
112
|
+
}
|
|
113
|
+
interface GenerateSingleCodeInput {
|
|
114
|
+
programId: string;
|
|
115
|
+
code?: string;
|
|
116
|
+
customerId?: string;
|
|
117
|
+
expiresAt?: Date;
|
|
118
|
+
initialBalance?: number;
|
|
119
|
+
metadata?: Record<string, unknown>;
|
|
120
|
+
}
|
|
121
|
+
interface RedeemVoucherInput {
|
|
122
|
+
code: string;
|
|
123
|
+
orderId: string;
|
|
124
|
+
customerId?: string;
|
|
125
|
+
discountAmount: number;
|
|
126
|
+
idempotencyKey?: string;
|
|
127
|
+
}
|
|
128
|
+
interface GiftCardSpendInput {
|
|
129
|
+
code: string;
|
|
130
|
+
amount: number;
|
|
131
|
+
orderId: string;
|
|
132
|
+
description?: string;
|
|
133
|
+
idempotencyKey?: string;
|
|
134
|
+
}
|
|
135
|
+
interface GiftCardTopUpInput {
|
|
136
|
+
code: string;
|
|
137
|
+
amount: number;
|
|
138
|
+
description?: string;
|
|
139
|
+
idempotencyKey?: string;
|
|
140
|
+
}
|
|
141
|
+
interface CartItem {
|
|
142
|
+
productId: string;
|
|
143
|
+
sku?: string;
|
|
144
|
+
categoryId?: string;
|
|
145
|
+
quantity: number;
|
|
146
|
+
unitPrice: number;
|
|
147
|
+
lineTotal?: number;
|
|
148
|
+
}
|
|
149
|
+
interface EvaluateInput {
|
|
150
|
+
items: CartItem[];
|
|
151
|
+
subtotal: number;
|
|
152
|
+
codes?: string[];
|
|
153
|
+
customerId?: string;
|
|
154
|
+
customerTags?: string[];
|
|
155
|
+
}
|
|
156
|
+
interface ListQuery {
|
|
157
|
+
page?: number;
|
|
158
|
+
limit?: number;
|
|
159
|
+
sort?: string;
|
|
160
|
+
filters?: Record<string, unknown>;
|
|
161
|
+
}
|
|
162
|
+
//#endregion
|
|
163
|
+
//#region src/types/results.d.ts
|
|
164
|
+
interface DiscountLine {
|
|
165
|
+
programId: string;
|
|
166
|
+
programName: string;
|
|
167
|
+
rewardId: string;
|
|
168
|
+
type: 'percentage' | 'fixed';
|
|
169
|
+
scope: 'order' | 'cheapest' | 'specific_products';
|
|
170
|
+
amount: number;
|
|
171
|
+
description: string;
|
|
172
|
+
voucherCode?: string;
|
|
173
|
+
}
|
|
174
|
+
interface FreeProductLine {
|
|
175
|
+
programId: string;
|
|
176
|
+
programName: string;
|
|
177
|
+
rewardId: string;
|
|
178
|
+
productId?: string;
|
|
179
|
+
productSku?: string;
|
|
180
|
+
quantity: number;
|
|
181
|
+
description: string;
|
|
182
|
+
}
|
|
183
|
+
interface RejectedCode {
|
|
184
|
+
code: string;
|
|
185
|
+
reason: string;
|
|
186
|
+
}
|
|
187
|
+
interface EvaluationResult {
|
|
188
|
+
evaluationId: string;
|
|
189
|
+
/**
|
|
190
|
+
* Deterministic hash of the evaluated cart (items + subtotal + codes +
|
|
191
|
+
* customerId). Hosts that want tamper-proof commits pass this hash back
|
|
192
|
+
* to `EvaluationService.commit(evaluationId, orderId, ctx, { cartHash })`;
|
|
193
|
+
* the engine throws `CartHashMismatchError` when the caller's hash does
|
|
194
|
+
* not match what was evaluated. Omitting the hash at commit keeps the
|
|
195
|
+
* legacy "trusted host" behaviour.
|
|
196
|
+
*/
|
|
197
|
+
cartHash: string;
|
|
198
|
+
appliedDiscounts: DiscountLine[];
|
|
199
|
+
freeProducts: FreeProductLine[];
|
|
200
|
+
totalDiscount: number;
|
|
201
|
+
subtotalAfterDiscount: number;
|
|
202
|
+
appliedCodes: string[];
|
|
203
|
+
rejectedCodes: RejectedCode[];
|
|
204
|
+
warnings: string[];
|
|
205
|
+
isPreview: boolean;
|
|
206
|
+
programsApplied: string[];
|
|
207
|
+
}
|
|
208
|
+
interface CommitResult {
|
|
209
|
+
evaluationId: string;
|
|
210
|
+
orderId: string;
|
|
211
|
+
totalDiscount: number;
|
|
212
|
+
programsCommitted: number;
|
|
213
|
+
vouchersUsed: number;
|
|
214
|
+
}
|
|
215
|
+
interface VoucherValidation {
|
|
216
|
+
valid: boolean;
|
|
217
|
+
voucher?: {
|
|
218
|
+
code: string;
|
|
219
|
+
programId: string;
|
|
220
|
+
programType: string;
|
|
221
|
+
status: string;
|
|
222
|
+
remainingUses: number;
|
|
223
|
+
currentBalance?: number;
|
|
224
|
+
expiresAt?: Date;
|
|
225
|
+
};
|
|
226
|
+
error?: string;
|
|
227
|
+
}
|
|
228
|
+
interface GiftCardBalance {
|
|
229
|
+
code: string;
|
|
230
|
+
initialBalance: number;
|
|
231
|
+
currentBalance: number;
|
|
232
|
+
spent: number;
|
|
233
|
+
voucherId: string;
|
|
234
|
+
}
|
|
235
|
+
interface PaginatedResult<T> {
|
|
236
|
+
docs: T[];
|
|
237
|
+
page: number;
|
|
238
|
+
limit: number;
|
|
239
|
+
total: number;
|
|
240
|
+
pages: number;
|
|
241
|
+
hasNext: boolean;
|
|
242
|
+
hasPrev: boolean;
|
|
243
|
+
}
|
|
244
|
+
//#endregion
|
|
245
|
+
//#region src/domain/ports/evaluation-store.port.d.ts
|
|
246
|
+
/**
|
|
247
|
+
* Snapshot the engine writes after `evaluate` and reads back at `commit`
|
|
248
|
+
* or `rollback`. Carries the materialised discount + the per-program /
|
|
249
|
+
* per-voucher usages the commit step must increment atomically.
|
|
250
|
+
*
|
|
251
|
+
* Mirrors the in-memory `StoredEvaluation` shape but moved out of the
|
|
252
|
+
* service file so storage adapters (Mongo, Redis, host-supplied) share
|
|
253
|
+
* one definition.
|
|
254
|
+
*/
|
|
255
|
+
interface StoredEvaluationSnapshot {
|
|
256
|
+
readonly result: EvaluationResult;
|
|
257
|
+
readonly ctx: PromoContext;
|
|
258
|
+
readonly customerId?: string;
|
|
259
|
+
readonly programUsages: ReadonlyArray<{
|
|
260
|
+
programId: string;
|
|
261
|
+
}>;
|
|
262
|
+
readonly voucherUsages: ReadonlyArray<{
|
|
263
|
+
voucherId: string;
|
|
264
|
+
code: string;
|
|
265
|
+
discountAmount: number;
|
|
266
|
+
}>;
|
|
267
|
+
readonly cartHash: string;
|
|
268
|
+
readonly createdAt: Date;
|
|
269
|
+
}
|
|
270
|
+
/**
|
|
271
|
+
* Per-call context for store operations. Carries:
|
|
272
|
+
* - `tenantValue` — the tenant scope value (e.g. an org id, branch id,
|
|
273
|
+
* workspace id). The store maps this onto whatever field name the
|
|
274
|
+
* engine's resolved TenantConfig declared (`organizationId`,
|
|
275
|
+
* `branchId`, `tenantId`, etc.). Storage adapters that don't tenant-
|
|
276
|
+
* scope simply ignore this.
|
|
277
|
+
* - `session` — Mongoose `ClientSession`. When set, the store
|
|
278
|
+
* operation joins the caller's transaction so failures roll back
|
|
279
|
+
* the read-and-delete atomically (the snapshot stays in the store
|
|
280
|
+
* and the caller can retry).
|
|
281
|
+
*
|
|
282
|
+
* The shape is intentionally tenant-agnostic — it carries the VALUE
|
|
283
|
+
* but not the FIELD NAME, so hosts on `branchId`/`tenantId`/etc. don't
|
|
284
|
+
* have to translate between context shapes at the call site.
|
|
285
|
+
*/
|
|
286
|
+
interface EvaluationStoreContext {
|
|
287
|
+
readonly tenantValue?: string;
|
|
288
|
+
readonly session?: unknown;
|
|
289
|
+
}
|
|
290
|
+
/**
|
|
291
|
+
* Pluggable backing store for pending evaluation snapshots.
|
|
292
|
+
*
|
|
293
|
+
* **Why a port, not a hard-coded Map:** the previous implementation kept
|
|
294
|
+
* pending evaluations in a process-local `Map<string, StoredEvaluation>`.
|
|
295
|
+
* That works for a single long-lived dev server but breaks every real
|
|
296
|
+
* production topology:
|
|
297
|
+
* - **Process restart** — every in-flight checkout's pending snapshot
|
|
298
|
+
* vanishes; subsequent commit() calls throw `EvaluationNotFoundError`.
|
|
299
|
+
* - **Horizontal scaling** — pod A's evaluate() snapshot is invisible
|
|
300
|
+
* to pod B's commit(); load-balanced traffic randomly fails.
|
|
301
|
+
* - **Serverless** — every cold start drops the Map; fits the worst
|
|
302
|
+
* pattern of the topology.
|
|
303
|
+
* - **Background worker handoff** — main pod evaluates, worker pod
|
|
304
|
+
* commits asynchronously: never works.
|
|
305
|
+
* - **Test isolation** — Map persists across vitest cases sharing the
|
|
306
|
+
* same engine singleton, polluting tests that should be independent.
|
|
307
|
+
*
|
|
308
|
+
* The port lets the package ship a default Mongo-backed store (works
|
|
309
|
+
* everywhere mongokit already runs) while letting hosts plug in Redis,
|
|
310
|
+
* DynamoDB, or in-memory implementations as their topology demands.
|
|
311
|
+
*
|
|
312
|
+
* Contract notes:
|
|
313
|
+
* - `take()` is **read-and-delete atomically**. This is the storage
|
|
314
|
+
* defence for "no double-commit on the same evaluationId" — even if
|
|
315
|
+
* the cap-CAS in the program repo were bypassed somehow, two
|
|
316
|
+
* concurrent commit() calls cannot both observe the same snapshot.
|
|
317
|
+
* - `take()` MUST honour `ctx.session` when provided. The service
|
|
318
|
+
* calls `take` INSIDE its transaction so a transient transaction
|
|
319
|
+
* failure rolls back the delete, leaving the snapshot for retry.
|
|
320
|
+
* Without session-awareness, a transient DB error would consume
|
|
321
|
+
* the snapshot permanently and force callers to re-evaluate.
|
|
322
|
+
* - `delete()` exists for explicit `rollback()`, where we want to drop
|
|
323
|
+
* the snapshot without consuming it.
|
|
324
|
+
* - `put()` carries a TTL. The store SHOULD honour it (Mongo TTL index,
|
|
325
|
+
* Redis EXPIRE, etc.) but the engine never relies on TTL alone for
|
|
326
|
+
* correctness — it always checks `take()` returned a fresh snapshot.
|
|
327
|
+
* - All methods take an optional `ctx` for tenant scoping. The store
|
|
328
|
+
* applies the configured tenant field to its read/write filters so
|
|
329
|
+
* pod A's snapshot in tenant X cannot be taken by a request in
|
|
330
|
+
* tenant Y.
|
|
331
|
+
*/
|
|
332
|
+
interface EvaluationStore {
|
|
333
|
+
/**
|
|
334
|
+
* Persist a fresh evaluation snapshot. The store is expected to expire
|
|
335
|
+
* the entry after `ttlSeconds` so abandoned evaluations don't grow
|
|
336
|
+
* unbounded. Implementations MUST allow re-puts on the same id (a host
|
|
337
|
+
* that re-evaluates the same cart should see the latest snapshot).
|
|
338
|
+
*/
|
|
339
|
+
put(id: string, snapshot: StoredEvaluationSnapshot, ttlSeconds: number, ctx?: EvaluationStoreContext): Promise<void>;
|
|
340
|
+
/**
|
|
341
|
+
* Atomically read AND delete the snapshot in a single operation. Used by
|
|
342
|
+
* `commit()` so two concurrent commits on the same evaluationId can
|
|
343
|
+
* never both succeed at the storage layer (one wins, the other gets
|
|
344
|
+
* `null` and surfaces as `EvaluationNotFoundError`).
|
|
345
|
+
*
|
|
346
|
+
* Honours `ctx.session` to participate in the caller's transaction —
|
|
347
|
+
* essential for transient-failure recovery (the delete rolls back if
|
|
348
|
+
* the transaction aborts, leaving the snapshot intact for retry).
|
|
349
|
+
*/
|
|
350
|
+
take(id: string, ctx?: EvaluationStoreContext): Promise<StoredEvaluationSnapshot | null>;
|
|
351
|
+
/**
|
|
352
|
+
* Drop the snapshot without consuming it. Used by `rollback()`.
|
|
353
|
+
* Idempotent: if the entry doesn't exist, return without error.
|
|
354
|
+
*/
|
|
355
|
+
delete(id: string, ctx?: EvaluationStoreContext): Promise<void>;
|
|
356
|
+
}
|
|
357
|
+
//#endregion
|
|
358
|
+
//#region src/types/config.d.ts
|
|
359
|
+
type TenantConfig$1 = false | TenantConfig;
|
|
360
|
+
interface IndexDefinition {
|
|
361
|
+
fields: Record<string, 1 | -1>;
|
|
362
|
+
options?: Record<string, unknown> | undefined;
|
|
363
|
+
}
|
|
364
|
+
type ModelName = 'program' | 'rule' | 'reward' | 'voucher';
|
|
365
|
+
type PluginType = PluginType$1;
|
|
366
|
+
interface RepositoryPlugins {
|
|
367
|
+
program?: PluginType$1[] | undefined;
|
|
368
|
+
rule?: PluginType$1[] | undefined;
|
|
369
|
+
reward?: PluginType$1[] | undefined;
|
|
370
|
+
voucher?: PluginType$1[] | undefined;
|
|
371
|
+
}
|
|
372
|
+
interface PromoConfig {
|
|
373
|
+
mongoose: Connection;
|
|
374
|
+
tenant?: TenantConfig$1 | undefined;
|
|
375
|
+
evaluation?: {
|
|
376
|
+
maxStackablePromotions?: number | undefined;
|
|
377
|
+
allowExclusiveAndStackable?: boolean | undefined;
|
|
378
|
+
} | undefined;
|
|
379
|
+
voucher?: {
|
|
380
|
+
codeLength?: number | undefined;
|
|
381
|
+
codePrefix?: string | undefined;
|
|
382
|
+
defaultExpiryDays?: number | undefined;
|
|
383
|
+
} | undefined;
|
|
384
|
+
giftCard?: {
|
|
385
|
+
allowNegativeBalance?: boolean | undefined;
|
|
386
|
+
maxBalance?: number | undefined;
|
|
387
|
+
} | undefined;
|
|
388
|
+
indexes?: Partial<Record<ModelName, IndexDefinition[]>> | undefined;
|
|
389
|
+
autoIndex?: boolean | Partial<Record<ModelName, boolean>> | undefined;
|
|
390
|
+
events?: {
|
|
391
|
+
transport?: EventTransport$1 | undefined;
|
|
392
|
+
} | undefined;
|
|
393
|
+
/**
|
|
394
|
+
* Host-provided outbox store (PACKAGE_RULES §5.5 + §P8). When wired, every
|
|
395
|
+
* domain event is persisted to the outbox inside the caller's `ctx.session`
|
|
396
|
+
* BEFORE being published to the transport — giving at-least-once delivery
|
|
397
|
+
* via the host-side relay. Absent → transport-only best-effort.
|
|
398
|
+
*
|
|
399
|
+
* Explicit `| undefined` per `exactOptionalPropertyTypes` convention.
|
|
400
|
+
*/
|
|
401
|
+
outbox?: OutboxStore | undefined;
|
|
402
|
+
/**
|
|
403
|
+
* Optional logger for dispatch-layer errors. Used by the shared
|
|
404
|
+
* `dispatch()` helper to surface outbox/transport failures without
|
|
405
|
+
* aborting the domain mutation. Defaults to `console`.
|
|
406
|
+
*/
|
|
407
|
+
logger?: {
|
|
408
|
+
error(message: string, ...args: unknown[]): void;
|
|
409
|
+
} | undefined;
|
|
410
|
+
plugins?: RepositoryPlugins | undefined;
|
|
411
|
+
/**
|
|
412
|
+
* Override the pending-evaluation backing store. Defaults to a
|
|
413
|
+
* Mongo-backed implementation over the engine's own
|
|
414
|
+
* `pendingEvaluation` repository — works on every topology mongokit
|
|
415
|
+
* already runs on (single process, horizontal scaling, serverless,
|
|
416
|
+
* worker handoff).
|
|
417
|
+
*
|
|
418
|
+
* Hosts on Redis, DynamoDB, an existing cache layer, or any custom
|
|
419
|
+
* backend implement `EvaluationStore` and pass it here. The package
|
|
420
|
+
* never assumes one backend; the port is stable, the default is
|
|
421
|
+
* convenient.
|
|
422
|
+
*/
|
|
423
|
+
evaluationStore?: EvaluationStore | undefined;
|
|
424
|
+
}
|
|
425
|
+
type ResolvedTenant = ResolvedTenantConfig;
|
|
426
|
+
interface ResolvedConfig {
|
|
427
|
+
evaluation: {
|
|
428
|
+
maxStackablePromotions: number;
|
|
429
|
+
allowExclusiveAndStackable: boolean;
|
|
430
|
+
};
|
|
431
|
+
voucher: {
|
|
432
|
+
codeLength: number;
|
|
433
|
+
codePrefix: string;
|
|
434
|
+
defaultExpiryDays: number | null;
|
|
435
|
+
};
|
|
436
|
+
giftCard: {
|
|
437
|
+
allowNegativeBalance: boolean;
|
|
438
|
+
maxBalance: number | null;
|
|
439
|
+
};
|
|
440
|
+
tenant: ResolvedTenant;
|
|
441
|
+
}
|
|
442
|
+
//#endregion
|
|
443
|
+
//#region src/models/create-models.d.ts
|
|
444
|
+
interface PromoModels {
|
|
445
|
+
Program: Model<unknown>;
|
|
446
|
+
Rule: Model<unknown>;
|
|
447
|
+
Reward: Model<unknown>;
|
|
448
|
+
Voucher: Model<unknown>;
|
|
449
|
+
PendingEvaluation: Model<unknown>;
|
|
450
|
+
}
|
|
451
|
+
//#endregion
|
|
452
|
+
//#region src/events/dispatch.d.ts
|
|
453
|
+
interface DispatchLogger {
|
|
454
|
+
error(message: string, ...args: unknown[]): void;
|
|
455
|
+
}
|
|
456
|
+
interface DispatchDeps {
|
|
457
|
+
events?: EventTransport$1 | undefined;
|
|
458
|
+
outbox?: OutboxStore | undefined;
|
|
459
|
+
logger?: DispatchLogger | undefined;
|
|
460
|
+
}
|
|
461
|
+
//#endregion
|
|
462
|
+
//#region src/repositories/pending-evaluation.repository.d.ts
|
|
463
|
+
/**
|
|
464
|
+
* Persistence-layer document shape — flat blob of the snapshot fields
|
|
465
|
+
* the schema declares. The service layer maps to/from
|
|
466
|
+
* `StoredEvaluationSnapshot` so the storage shape stays decoupled from
|
|
467
|
+
* the domain shape.
|
|
468
|
+
*/
|
|
469
|
+
interface PendingEvaluationDocument {
|
|
470
|
+
_id: string;
|
|
471
|
+
evaluationId: string;
|
|
472
|
+
result: Record<string, unknown>;
|
|
473
|
+
ctx: Record<string, unknown>;
|
|
474
|
+
customerId: string | null;
|
|
475
|
+
programUsages: Array<Record<string, unknown>>;
|
|
476
|
+
voucherUsages: Array<Record<string, unknown>>;
|
|
477
|
+
cartHash: string;
|
|
478
|
+
expiresAt: Date;
|
|
479
|
+
createdAt: Date;
|
|
480
|
+
updatedAt: Date;
|
|
481
|
+
}
|
|
482
|
+
/**
|
|
483
|
+
* Pending-evaluation repository. Extends mongokit's `Repository<TDoc>`
|
|
484
|
+
* directly per package rules (no service wrapper, no aliased verbs).
|
|
485
|
+
* Adds one custom domain method: `takeByEvaluationId` — atomic
|
|
486
|
+
* read-and-delete via raw `Model.findOneAndDelete`.
|
|
487
|
+
*
|
|
488
|
+
* **Why raw `findOneAndDelete` (escape from mongokit's `delete()`)**:
|
|
489
|
+
* mongokit's `Repository.delete()` returns `{success, message}` only —
|
|
490
|
+
* it doesn't surface the deleted document. `take` semantics require
|
|
491
|
+
* "atomically remove AND return", which is the canonical defence
|
|
492
|
+
* against double-commit on the same evaluationId at the storage layer
|
|
493
|
+
* (one caller wins the document, the other gets `null`). This is the
|
|
494
|
+
* narrow exception PACKAGE_RULES.md / order/CLAUDE.md sanction:
|
|
495
|
+
* *"Raw findOneAndUpdate/findOneAndDelete is allowed ONLY for atomic
|
|
496
|
+
* state-machine transitions — flag each one with a comment."*
|
|
497
|
+
*/
|
|
498
|
+
declare class PendingEvaluationRepository extends Repository<PendingEvaluationDocument> {
|
|
499
|
+
/**
|
|
500
|
+
* The repository owns its tenant config so its raw-driver methods
|
|
501
|
+
* (`findOneAndDelete`, `deleteOne`) can apply the SAME scoping rule
|
|
502
|
+
* the mongokit hook pipeline would apply on standard methods —
|
|
503
|
+
* specifically using `tenant.tenantField` (host-configurable as
|
|
504
|
+
* `organizationId`, `branchId`, `tenantId`, etc.) NOT a hardcoded
|
|
505
|
+
* `organizationId`. Without this, deployments that configure custom
|
|
506
|
+
* tenant fields would silently lose isolation on the cache layer.
|
|
507
|
+
*/
|
|
508
|
+
private readonly tenant;
|
|
509
|
+
constructor(model: Model<PendingEvaluationDocument>, plugins?: PluginType$1[], tenant?: ResolvedTenant);
|
|
510
|
+
/** The host-configured tenant field name (or `undefined` if single-tenant). */
|
|
511
|
+
get tenantField(): string | undefined;
|
|
512
|
+
/**
|
|
513
|
+
* Atomic read-and-delete by evaluationId. Two concurrent commit calls
|
|
514
|
+
* on the same id race here at the database layer — the winner gets
|
|
515
|
+
* the document, the loser gets `null` and the calling service throws
|
|
516
|
+
* `EvaluationNotFoundError`. No way both succeed.
|
|
517
|
+
*
|
|
518
|
+
* Honours `ctx.session` so the operation joins the caller's
|
|
519
|
+
* transaction. If the transaction aborts (transient DB error, cap
|
|
520
|
+
* exceeded, etc.) the delete rolls back and the snapshot stays in
|
|
521
|
+
* the store — letting the caller retry without re-evaluation.
|
|
522
|
+
*
|
|
523
|
+
* Tenant scoping uses the configured `tenant.tenantField` (NOT a
|
|
524
|
+
* hardcoded `organizationId`) so hosts on `branchId`/`tenantId`/etc.
|
|
525
|
+
* keep cross-tenant isolation on the cache layer too.
|
|
526
|
+
*/
|
|
527
|
+
takeByEvaluationId(evaluationId: string, ctx?: {
|
|
528
|
+
tenantValue?: string;
|
|
529
|
+
session?: ClientSession;
|
|
530
|
+
}): Promise<PendingEvaluationDocument | null>;
|
|
531
|
+
/**
|
|
532
|
+
* Idempotent delete. Returns whether anything was actually removed,
|
|
533
|
+
* so callers can distinguish "we cleaned it" from "already gone".
|
|
534
|
+
*/
|
|
535
|
+
deleteByEvaluationId(evaluationId: string, ctx?: {
|
|
536
|
+
tenantValue?: string;
|
|
537
|
+
session?: ClientSession;
|
|
538
|
+
}): Promise<boolean>;
|
|
539
|
+
/**
|
|
540
|
+
* Build a query filter with the configured tenant scope appended,
|
|
541
|
+
* mirroring what mongokit's `multiTenantPlugin` injects on standard
|
|
542
|
+
* Repository methods. We re-implement here because raw driver calls
|
|
543
|
+
* (`findOneAndDelete`, `deleteOne`) bypass the plugin pipeline.
|
|
544
|
+
*/
|
|
545
|
+
private scopedFilter;
|
|
546
|
+
}
|
|
547
|
+
//#endregion
|
|
548
|
+
//#region src/domain/entities/program.d.ts
|
|
549
|
+
interface Program {
|
|
550
|
+
_id: string;
|
|
551
|
+
name: string;
|
|
552
|
+
description?: string;
|
|
553
|
+
programType: ProgramType;
|
|
554
|
+
triggerMode: TriggerMode;
|
|
555
|
+
status: ProgramStatus;
|
|
556
|
+
stackingMode: StackingMode;
|
|
557
|
+
priority: number;
|
|
558
|
+
startsAt?: Date;
|
|
559
|
+
endsAt?: Date;
|
|
560
|
+
maxUsageTotal?: number;
|
|
561
|
+
usedCount: number;
|
|
562
|
+
maxUsagePerCustomer?: number;
|
|
563
|
+
applicableCustomerIds: string[];
|
|
564
|
+
applicableCustomerTags: string[];
|
|
565
|
+
customerUsageCounts?: Record<string, number>;
|
|
566
|
+
metadata?: Record<string, unknown>;
|
|
567
|
+
createdAt: Date;
|
|
568
|
+
updatedAt: Date;
|
|
569
|
+
}
|
|
570
|
+
//#endregion
|
|
571
|
+
//#region src/repositories/program.repository.d.ts
|
|
572
|
+
declare class ProgramRepository extends Repository<Program> {
|
|
573
|
+
private dispatchDeps;
|
|
574
|
+
constructor(model: Model<Program>, plugins?: PluginType$1[], dispatchDeps?: DispatchDeps);
|
|
575
|
+
activate(id: string, ctx: PromoContext): Promise<Program>;
|
|
576
|
+
pause(id: string, ctx: PromoContext): Promise<Program>;
|
|
577
|
+
archive(id: string, ctx: PromoContext): Promise<Program>;
|
|
578
|
+
private _transition;
|
|
579
|
+
incrementUsage(id: string, ctx?: Record<string, unknown>): Promise<Program>;
|
|
580
|
+
/**
|
|
581
|
+
* Atomic compare-and-set increment: succeeds only if the program either
|
|
582
|
+
* has no cap (`maxUsageTotal == null`) OR `usedCount < maxUsageTotal`.
|
|
583
|
+
* If the cap is already saturated, returns `null` so the caller can
|
|
584
|
+
* decide whether to throw (commit-time enforcement) or skip silently
|
|
585
|
+
* (best-effort eligibility check).
|
|
586
|
+
*
|
|
587
|
+
* Industry-standard primitive for "promo with finite supply" — without
|
|
588
|
+
* this, two evaluations both see the program as available and both
|
|
589
|
+
* commit, leading to oversell. The atomic filter on the same write
|
|
590
|
+
* eliminates the race entirely; concurrent losers see a `null` return
|
|
591
|
+
* and can downgrade their commit (apply order without the discount,
|
|
592
|
+
* surface "promo no longer available" to the user, etc.).
|
|
593
|
+
*
|
|
594
|
+
* Routes through mongokit's `update` so tenant scoping + hooks fire.
|
|
595
|
+
*/
|
|
596
|
+
tryIncrementUsage(id: string, ctx?: Record<string, unknown>): Promise<Program | null>;
|
|
597
|
+
decrementUsage(id: string, ctx?: Record<string, unknown>): Promise<Program>;
|
|
598
|
+
getCustomerUsage(id: string, customerId: string, ctx?: Record<string, unknown>): Promise<number>;
|
|
599
|
+
incrementCustomerUsage(id: string, customerId: string, ctx?: Record<string, unknown>): Promise<Program>;
|
|
600
|
+
findActive(now?: Date, ctx?: Record<string, unknown>): Promise<Program[]>;
|
|
601
|
+
}
|
|
602
|
+
//#endregion
|
|
603
|
+
//#region src/domain/entities/reward.d.ts
|
|
604
|
+
interface Reward {
|
|
605
|
+
_id: string;
|
|
606
|
+
programId: string;
|
|
607
|
+
ruleId?: string;
|
|
608
|
+
rewardType: RewardType;
|
|
609
|
+
discountMode?: DiscountMode;
|
|
610
|
+
discountAmount?: number;
|
|
611
|
+
maxDiscountAmount?: number;
|
|
612
|
+
discountScope: DiscountScope;
|
|
613
|
+
applicableProductIds: string[];
|
|
614
|
+
freeProductId?: string;
|
|
615
|
+
freeProductSku?: string;
|
|
616
|
+
freeQuantity: number;
|
|
617
|
+
giftCardAmount?: number;
|
|
618
|
+
metadata?: Record<string, unknown>;
|
|
619
|
+
createdAt: Date;
|
|
620
|
+
updatedAt: Date;
|
|
621
|
+
}
|
|
622
|
+
//#endregion
|
|
623
|
+
//#region src/repositories/reward.repository.d.ts
|
|
624
|
+
declare class RewardRepository extends Repository<Reward> {
|
|
625
|
+
constructor(model: Model<Reward>, plugins?: PluginType$1[]);
|
|
626
|
+
}
|
|
627
|
+
//#endregion
|
|
628
|
+
//#region src/domain/entities/rule.d.ts
|
|
629
|
+
interface Rule {
|
|
630
|
+
_id: string;
|
|
631
|
+
programId: string;
|
|
632
|
+
name?: string;
|
|
633
|
+
minimumAmount: number;
|
|
634
|
+
minimumQuantity: number;
|
|
635
|
+
applicableProductIds: string[];
|
|
636
|
+
applicableCategories: string[];
|
|
637
|
+
applicableSkus: string[];
|
|
638
|
+
buyQuantity?: number;
|
|
639
|
+
code?: string;
|
|
640
|
+
startsAt?: Date;
|
|
641
|
+
endsAt?: Date;
|
|
642
|
+
metadata?: Record<string, unknown>;
|
|
643
|
+
createdAt: Date;
|
|
644
|
+
updatedAt: Date;
|
|
645
|
+
}
|
|
646
|
+
//#endregion
|
|
647
|
+
//#region src/repositories/rule.repository.d.ts
|
|
648
|
+
declare class RuleRepository extends Repository<Rule> {
|
|
649
|
+
constructor(model: Model<Rule>, plugins?: PluginType$1[]);
|
|
650
|
+
}
|
|
651
|
+
//#endregion
|
|
652
|
+
//#region src/domain/entities/voucher.d.ts
|
|
653
|
+
interface BalanceLedgerEntry {
|
|
654
|
+
amount: number;
|
|
655
|
+
orderId?: string;
|
|
656
|
+
description?: string;
|
|
657
|
+
createdAt: Date;
|
|
658
|
+
idempotencyKey?: string;
|
|
659
|
+
/** Branch / store / location where the spend or top-up was performed.
|
|
660
|
+
* Stamped from `ctx.organizationId` at write time. Vouchers themselves
|
|
661
|
+
* remain company-wide (`tenant: false` deployments included) — this
|
|
662
|
+
* field is purely for analytics ("redemptions per branch") and audit
|
|
663
|
+
* ("which staff branch credited this card?") without joining orders. */
|
|
664
|
+
organizationId?: string;
|
|
665
|
+
}
|
|
666
|
+
interface VoucherRedemption {
|
|
667
|
+
orderId: string;
|
|
668
|
+
customerId?: string;
|
|
669
|
+
discountAmount: number;
|
|
670
|
+
redeemedAt: Date;
|
|
671
|
+
idempotencyKey?: string;
|
|
672
|
+
/** Branch / store / location where the redemption happened. Stamped
|
|
673
|
+
* from `ctx.organizationId` at write time. See `BalanceLedgerEntry`. */
|
|
674
|
+
organizationId?: string;
|
|
675
|
+
}
|
|
676
|
+
interface Voucher {
|
|
677
|
+
_id: string;
|
|
678
|
+
programId: string;
|
|
679
|
+
code: string;
|
|
680
|
+
status: VoucherStatus;
|
|
681
|
+
customerId?: string;
|
|
682
|
+
usageLimit: number;
|
|
683
|
+
usedCount: number;
|
|
684
|
+
initialBalance?: number;
|
|
685
|
+
currentBalance?: number;
|
|
686
|
+
balanceLedger: BalanceLedgerEntry[];
|
|
687
|
+
expiresAt?: Date;
|
|
688
|
+
redemptions: VoucherRedemption[];
|
|
689
|
+
metadata?: Record<string, unknown>;
|
|
690
|
+
createdAt: Date;
|
|
691
|
+
updatedAt: Date;
|
|
692
|
+
}
|
|
693
|
+
//#endregion
|
|
694
|
+
//#region src/repositories/voucher.repository.d.ts
|
|
695
|
+
declare class VoucherRepository extends Repository<Voucher> {
|
|
696
|
+
private dispatchDeps;
|
|
697
|
+
private tenantField;
|
|
698
|
+
private tenantEnabled;
|
|
699
|
+
constructor(model: Model<Voucher>, plugins?: PluginType$1[], dispatchDeps?: DispatchDeps, tenantField?: string, tenantEnabled?: boolean);
|
|
700
|
+
/**
|
|
701
|
+
* Copy the tenant id from `ctx` onto the write payload so the doc persists
|
|
702
|
+
* with the correct `organizationId` even when the host has opted OUT of the
|
|
703
|
+
* auto-wired `multiTenantPlugin` but still scopes at its framework layer
|
|
704
|
+
* (e.g. arc's preset + `BaseController` — see `@classytic/order` CLAUDE.md:
|
|
705
|
+
* "Child repos set `organizationId` explicitly on the doc").
|
|
706
|
+
*
|
|
707
|
+
* Skipped entirely when the engine was configured with `tenant: false` —
|
|
708
|
+
* the host's intent is company-wide rows (no `organizationId` on disk),
|
|
709
|
+
* and injecting it from `ctx` would silently re-scope writes per branch
|
|
710
|
+
* while reads still look at the unscoped collection.
|
|
711
|
+
*/
|
|
712
|
+
private _injectTenant;
|
|
713
|
+
create(data: Parameters<Repository<Voucher>['create']>[0], options?: Parameters<Repository<Voucher>['create']>[1]): Promise<Voucher>;
|
|
714
|
+
createMany(docs: Parameters<Repository<Voucher>['createMany']>[0], options?: Parameters<Repository<Voucher>['createMany']>[1]): Promise<Voucher[]>;
|
|
715
|
+
/** Domain verb: cancel a voucher (status transition + event). */
|
|
716
|
+
cancel(id: string, ctx: PromoContext): Promise<Voucher>;
|
|
717
|
+
/** Atomic usage increment + push redemption record. */
|
|
718
|
+
incrementUsage(id: string, redemption: Record<string, unknown>, ctx?: Record<string, unknown>): Promise<Voucher>;
|
|
719
|
+
/** Atomic balance delta + push ledger entry. */
|
|
720
|
+
addLedgerEntry(id: string, entry: Record<string, unknown>, balanceDelta: number, ctx?: Record<string, unknown>): Promise<Voucher>;
|
|
721
|
+
/**
|
|
722
|
+
* Batch expire by date.
|
|
723
|
+
*
|
|
724
|
+
* Routes through `findAll` + per-doc `update` instead of a raw
|
|
725
|
+
* `this.Model.updateMany`. Rationale:
|
|
726
|
+
* 1. `findAll` casts the filter through the schema (so an ObjectId
|
|
727
|
+
* tenant field on a write-time String context doesn't silently
|
|
728
|
+
* return 0 rows the way a raw `$match` would).
|
|
729
|
+
* 2. `update` fires the full hook pipeline (multi-tenant, audit,
|
|
730
|
+
* soft-delete) on every expiration — a bulk `updateMany` at the
|
|
731
|
+
* Model layer bypasses all of them.
|
|
732
|
+
* Volume is bounded per tenant (active + past-expiry vouchers), so an
|
|
733
|
+
* N+1 loop is acceptable for correctness.
|
|
734
|
+
*/
|
|
735
|
+
expireByDate(before: Date, ctx?: Record<string, unknown>): Promise<number>;
|
|
736
|
+
/**
|
|
737
|
+
* Lookup voucher by unique code, scoped to the tenant in `ctx` when
|
|
738
|
+
* tenant scoping is configured.
|
|
739
|
+
*
|
|
740
|
+
* Manual tenant injection mirrors `expireByDate` and `ProgramRepository.findActive`
|
|
741
|
+
* — it makes the method work whether the host opts into the auto-wired
|
|
742
|
+
* `multiTenantPlugin` or enforces scoping at its own framework layer
|
|
743
|
+
* (e.g. arc's preset + `BaseController`). Without this, a host that
|
|
744
|
+
* runs the plugin off would leak vouchers across branches at
|
|
745
|
+
* validate/redeem/spend/topUp call sites.
|
|
746
|
+
*
|
|
747
|
+
* When the engine is configured with `tenant: false`, the filter is
|
|
748
|
+
* code-only — vouchers are company-wide and the docs carry no
|
|
749
|
+
* `organizationId`, so injecting one would always miss.
|
|
750
|
+
*/
|
|
751
|
+
getByCode(code: string, ctx?: Record<string, unknown>): Promise<Voucher | null>;
|
|
752
|
+
/**
|
|
753
|
+
* Idempotency check on nested arrays. Routes through mongokit's `count`
|
|
754
|
+
* so `multiTenantPlugin` injects the configured tenant field from
|
|
755
|
+
* `ctx` — a non-tenant-scoped check would cross organization boundaries.
|
|
756
|
+
*/
|
|
757
|
+
hasIdempotencyKey(id: string, key: string, ctx?: Record<string, unknown>): Promise<boolean>;
|
|
758
|
+
}
|
|
759
|
+
//#endregion
|
|
760
|
+
//#region src/repositories/create-repositories.d.ts
|
|
761
|
+
interface PromoRepositories {
|
|
762
|
+
program: ProgramRepository;
|
|
763
|
+
rule: RuleRepository;
|
|
764
|
+
reward: RewardRepository;
|
|
765
|
+
voucher: VoucherRepository;
|
|
766
|
+
pendingEvaluation: PendingEvaluationRepository;
|
|
767
|
+
}
|
|
768
|
+
interface RepositoryPluginsMap {
|
|
769
|
+
program?: PluginType$1[];
|
|
770
|
+
rule?: PluginType$1[];
|
|
771
|
+
reward?: PluginType$1[];
|
|
772
|
+
voucher?: PluginType$1[];
|
|
773
|
+
pendingEvaluation?: PluginType$1[];
|
|
774
|
+
}
|
|
775
|
+
interface RepositoryDispatchDeps {
|
|
776
|
+
events?: EventTransport$1 | undefined;
|
|
777
|
+
outbox?: OutboxStore | undefined;
|
|
778
|
+
logger?: DispatchLogger | undefined;
|
|
779
|
+
}
|
|
780
|
+
//#endregion
|
|
781
|
+
//#region src/domain/ports/unit-of-work.port.d.ts
|
|
782
|
+
type TransactionSession = ClientSession;
|
|
783
|
+
interface UnitOfWork {
|
|
784
|
+
withTransaction<T>(cb: (session: TransactionSession) => Promise<T>): Promise<T>;
|
|
785
|
+
}
|
|
786
|
+
//#endregion
|
|
787
|
+
//#region src/services/evaluation.service.d.ts
|
|
788
|
+
interface CommitOptions {
|
|
789
|
+
/**
|
|
790
|
+
* The `cartHash` returned from `evaluate`. When provided, the engine
|
|
791
|
+
* verifies it matches the hash computed at evaluation time and throws
|
|
792
|
+
* `CartHashMismatchError` on mismatch. Guards against cart-tampering
|
|
793
|
+
* between evaluate and commit (e.g. user previews a big discount on a
|
|
794
|
+
* heavy cart, swaps the cart for a cheaper one, then commits the stale
|
|
795
|
+
* discount). Hosts that trust their own commit pipeline end-to-end may
|
|
796
|
+
* omit it for the legacy behaviour.
|
|
797
|
+
*/
|
|
798
|
+
cartHash?: string;
|
|
799
|
+
}
|
|
800
|
+
declare class EvaluationService {
|
|
801
|
+
private programRepo;
|
|
802
|
+
private ruleRepo;
|
|
803
|
+
private rewardRepo;
|
|
804
|
+
private voucherRepo;
|
|
805
|
+
private unitOfWork;
|
|
806
|
+
private dispatchDeps;
|
|
807
|
+
private config;
|
|
808
|
+
private store;
|
|
809
|
+
constructor(programRepo: ProgramRepository, ruleRepo: RuleRepository, rewardRepo: RewardRepository, voucherRepo: VoucherRepository, unitOfWork: UnitOfWork, dispatchDeps: DispatchDeps, config: ResolvedConfig, store: EvaluationStore);
|
|
810
|
+
evaluate(input: EvaluateInput, ctx: PromoContext): Promise<EvaluationResult>;
|
|
811
|
+
preview(input: EvaluateInput, ctx: PromoContext): Promise<EvaluationResult>;
|
|
812
|
+
commit(evaluationId: string, orderId: string, ctx: PromoContext, options?: CommitOptions): Promise<CommitResult>;
|
|
813
|
+
private commitInTransaction;
|
|
814
|
+
rollback(evaluationId: string, ctx: PromoContext): Promise<void>;
|
|
815
|
+
private doEvaluate;
|
|
816
|
+
private isCustomerEligible;
|
|
817
|
+
private matchBestRule;
|
|
818
|
+
private matchSingleRule;
|
|
819
|
+
private filterRewardsByRule;
|
|
820
|
+
private computeDiscount;
|
|
821
|
+
private getEligibleItems;
|
|
822
|
+
private describeDiscount;
|
|
823
|
+
}
|
|
824
|
+
//#endregion
|
|
825
|
+
//#region src/services/voucher.service.d.ts
|
|
826
|
+
/**
|
|
827
|
+
* VoucherService — multi-repo domain orchestration only.
|
|
828
|
+
*
|
|
829
|
+
* Code generation, redemption (transactional + idempotency), gift card
|
|
830
|
+
* spend/topUp, and validation live here because they need programRepo +
|
|
831
|
+
* voucherRepo + config + transactions.
|
|
832
|
+
*
|
|
833
|
+
* Simple lookups (getById, getByCode, getAll) and the cancel domain verb
|
|
834
|
+
* live on VoucherRepository directly — callers use the repo. §2/§3.
|
|
835
|
+
*/
|
|
836
|
+
declare class VoucherService {
|
|
837
|
+
private voucherRepo;
|
|
838
|
+
private programRepo;
|
|
839
|
+
private unitOfWork;
|
|
840
|
+
private dispatchDeps;
|
|
841
|
+
private config;
|
|
842
|
+
constructor(voucherRepo: VoucherRepository, programRepo: ProgramRepository, unitOfWork: UnitOfWork, dispatchDeps: DispatchDeps, config: ResolvedConfig);
|
|
843
|
+
generateCodes(input: GenerateCodesInput, ctx: PromoContext): Promise<Voucher[]>;
|
|
844
|
+
generateSingleCode(input: GenerateSingleCodeInput, ctx: PromoContext): Promise<Voucher>;
|
|
845
|
+
validateCode(code: string, ctx: PromoContext): Promise<VoucherValidation>;
|
|
846
|
+
redeem(input: RedeemVoucherInput, ctx: PromoContext): Promise<Voucher>;
|
|
847
|
+
getBalance(code: string, ctx: PromoContext): Promise<GiftCardBalance>;
|
|
848
|
+
spend(input: GiftCardSpendInput, ctx: PromoContext): Promise<GiftCardBalance>;
|
|
849
|
+
private spendInTransaction;
|
|
850
|
+
topUp(input: GiftCardTopUpInput, ctx: PromoContext): Promise<GiftCardBalance>;
|
|
851
|
+
private topUpInTransaction;
|
|
852
|
+
private assertVoucherUsable;
|
|
853
|
+
}
|
|
854
|
+
//#endregion
|
|
855
|
+
//#region src/services/create-services.d.ts
|
|
856
|
+
interface PromoServices {
|
|
857
|
+
voucher: VoucherService;
|
|
858
|
+
evaluation: EvaluationService;
|
|
859
|
+
}
|
|
860
|
+
//#endregion
|
|
861
|
+
//#region src/adapters/mongo-evaluation-store.d.ts
|
|
862
|
+
/**
|
|
863
|
+
* Default Mongo-backed implementation of {@link EvaluationStore}. Translates
|
|
864
|
+
* between the domain snapshot shape and the persistence document shape,
|
|
865
|
+
* forwards atomic `take`/`delete` semantics to the repository, and
|
|
866
|
+
* applies the engine's configured tenant field on writes (the repo
|
|
867
|
+
* applies it on reads).
|
|
868
|
+
*
|
|
869
|
+
* Hosts that prefer a different backend (Redis, DynamoDB, in-memory for
|
|
870
|
+
* tests) can implement `EvaluationStore` directly and pass it via engine
|
|
871
|
+
* config — this class isn't load-bearing.
|
|
872
|
+
*/
|
|
873
|
+
declare class MongoEvaluationStore implements EvaluationStore {
|
|
874
|
+
private readonly repo;
|
|
875
|
+
constructor(repo: PendingEvaluationRepository);
|
|
876
|
+
put(id: string, snapshot: StoredEvaluationSnapshot, ttlSeconds: number, ctx?: EvaluationStoreContext): Promise<void>;
|
|
877
|
+
take(id: string, ctx?: EvaluationStoreContext): Promise<StoredEvaluationSnapshot | null>;
|
|
878
|
+
delete(id: string, ctx?: EvaluationStoreContext): Promise<void>;
|
|
879
|
+
}
|
|
880
|
+
/**
|
|
881
|
+
* In-memory fallback. Useful for unit tests, single-process dev servers,
|
|
882
|
+
* and hosts that consciously opt out of persistence (knowing they lose
|
|
883
|
+
* pending evaluations on restart). NOT recommended for production
|
|
884
|
+
* topologies — see EvaluationStore docblock for why.
|
|
885
|
+
*
|
|
886
|
+
* Tenant scoping uses `ctx.tenantValue` as part of the in-memory key
|
|
887
|
+
* (the field-name layer doesn't matter here — we just namespace by
|
|
888
|
+
* value). No transaction semantics (in-memory has nothing to roll
|
|
889
|
+
* back), no TTL refinement beyond initial expiry check.
|
|
890
|
+
*/
|
|
891
|
+
declare class InMemoryEvaluationStore implements EvaluationStore {
|
|
892
|
+
private readonly map;
|
|
893
|
+
put(id: string, snapshot: StoredEvaluationSnapshot, ttlSeconds: number, ctx?: EvaluationStoreContext): Promise<void>;
|
|
894
|
+
take(id: string, ctx?: EvaluationStoreContext): Promise<StoredEvaluationSnapshot | null>;
|
|
895
|
+
delete(id: string, ctx?: EvaluationStoreContext): Promise<void>;
|
|
896
|
+
private key;
|
|
897
|
+
}
|
|
898
|
+
//#endregion
|
|
899
|
+
//#region src/domain/errors/base.d.ts
|
|
900
|
+
declare abstract class PromoError extends Error {
|
|
901
|
+
abstract readonly code: string;
|
|
902
|
+
constructor(message: string);
|
|
903
|
+
}
|
|
904
|
+
//#endregion
|
|
905
|
+
//#region src/domain/errors/domain-errors.d.ts
|
|
906
|
+
declare class ValidationError extends PromoError {
|
|
907
|
+
readonly code = "VALIDATION_ERROR";
|
|
908
|
+
}
|
|
909
|
+
declare class ProgramNotFoundError extends PromoError {
|
|
910
|
+
readonly code = "PROGRAM_NOT_FOUND";
|
|
911
|
+
constructor(id?: string);
|
|
912
|
+
}
|
|
913
|
+
/**
|
|
914
|
+
* Raised when a commit-time atomic CAS on `usedCount < maxUsageTotal`
|
|
915
|
+
* fails — i.e. the program's cap was exhausted by another concurrent
|
|
916
|
+
* commit between this caller's evaluate and its commit. The host should
|
|
917
|
+
* surface this as a "promo no longer available" failure to the user; the
|
|
918
|
+
* order itself can still proceed without the discount if the host wishes.
|
|
919
|
+
*/
|
|
920
|
+
declare class ProgramUsageCapExceededError extends PromoError {
|
|
921
|
+
readonly programId: string;
|
|
922
|
+
readonly maxUsageTotal: number;
|
|
923
|
+
readonly code = "PROGRAM_USAGE_CAP_EXCEEDED";
|
|
924
|
+
constructor(programId: string, maxUsageTotal: number);
|
|
925
|
+
}
|
|
926
|
+
declare class RuleNotFoundError extends PromoError {
|
|
927
|
+
readonly code = "RULE_NOT_FOUND";
|
|
928
|
+
constructor(id?: string);
|
|
929
|
+
}
|
|
930
|
+
declare class RewardNotFoundError extends PromoError {
|
|
931
|
+
readonly code = "REWARD_NOT_FOUND";
|
|
932
|
+
constructor(id?: string);
|
|
933
|
+
}
|
|
934
|
+
declare class VoucherNotFoundError extends PromoError {
|
|
935
|
+
readonly code = "VOUCHER_NOT_FOUND";
|
|
936
|
+
constructor(codeOrId?: string);
|
|
937
|
+
}
|
|
938
|
+
declare class InvalidTransitionError extends PromoError {
|
|
939
|
+
readonly code = "INVALID_TRANSITION";
|
|
940
|
+
constructor(from: string, to: string);
|
|
941
|
+
}
|
|
942
|
+
declare class VoucherExpiredError extends PromoError {
|
|
943
|
+
readonly code = "VOUCHER_EXPIRED";
|
|
944
|
+
constructor(code: string);
|
|
945
|
+
}
|
|
946
|
+
declare class VoucherExhaustedError extends PromoError {
|
|
947
|
+
readonly code = "VOUCHER_EXHAUSTED";
|
|
948
|
+
constructor(code: string);
|
|
949
|
+
}
|
|
950
|
+
/**
|
|
951
|
+
* Thrown when a gift card's balance has been fully spent and its status
|
|
952
|
+
* flipped to `'used'`. Distinct from `VoucherExhaustedError` (usage-limit
|
|
953
|
+
* exhaustion on a discount voucher) so hosts can show "top up" vs "retry
|
|
954
|
+
* later" messaging.
|
|
955
|
+
*/
|
|
956
|
+
declare class GiftCardExhaustedError extends PromoError {
|
|
957
|
+
readonly code = "GIFT_CARD_EXHAUSTED";
|
|
958
|
+
constructor(code: string);
|
|
959
|
+
}
|
|
960
|
+
/**
|
|
961
|
+
* Thrown when a MongoDB WriteConflict surfaces under voucher-spend
|
|
962
|
+
* contention. Losers see a stable domain shape rather than the raw
|
|
963
|
+
* `"Write conflict during plan execution"` string. Hosts can translate
|
|
964
|
+
* this to HTTP 409 + retry.
|
|
965
|
+
*/
|
|
966
|
+
declare class ConcurrencyConflictError extends PromoError {
|
|
967
|
+
readonly resource: 'voucher' | 'program' | 'rule' | 'reward';
|
|
968
|
+
readonly resourceId: string;
|
|
969
|
+
readonly cause?: unknown | undefined;
|
|
970
|
+
readonly code = "CONCURRENCY_CONFLICT";
|
|
971
|
+
readonly status = 409;
|
|
972
|
+
constructor(resource: 'voucher' | 'program' | 'rule' | 'reward', resourceId: string, cause?: unknown | undefined);
|
|
973
|
+
}
|
|
974
|
+
declare class InsufficientBalanceError extends PromoError {
|
|
975
|
+
readonly code = "INSUFFICIENT_BALANCE";
|
|
976
|
+
constructor(code: string, available: number, requested: number);
|
|
977
|
+
}
|
|
978
|
+
declare class TenantIsolationError extends PromoError {
|
|
979
|
+
readonly code = "TENANT_ISOLATION";
|
|
980
|
+
constructor();
|
|
981
|
+
}
|
|
982
|
+
declare class DuplicateRedemptionError extends PromoError {
|
|
983
|
+
readonly code = "DUPLICATE_REDEMPTION";
|
|
984
|
+
constructor(key: string);
|
|
985
|
+
}
|
|
986
|
+
declare class EvaluationNotFoundError extends PromoError {
|
|
987
|
+
readonly code = "EVALUATION_NOT_FOUND";
|
|
988
|
+
constructor(id: string);
|
|
989
|
+
}
|
|
990
|
+
/**
|
|
991
|
+
* Thrown by `EvaluationService.commit` when the cart hash provided by the
|
|
992
|
+
* caller does not match the cart hash computed at evaluation time. Guards
|
|
993
|
+
* against cart-tampering attacks where a user previews a large discount on
|
|
994
|
+
* a heavy cart, mutates the cart to something cheaper before committing, and
|
|
995
|
+
* tries to apply the stale discount to the altered order.
|
|
996
|
+
*/
|
|
997
|
+
declare class CartHashMismatchError extends PromoError {
|
|
998
|
+
readonly code = "CART_HASH_MISMATCH";
|
|
999
|
+
constructor(evaluationId: string);
|
|
1000
|
+
}
|
|
1001
|
+
//#endregion
|
|
1002
|
+
//#region src/events/event-constants.d.ts
|
|
1003
|
+
declare const PromoEvents: {
|
|
1004
|
+
readonly PROGRAM_CREATED: "promo.program.created";
|
|
1005
|
+
readonly PROGRAM_ACTIVATED: "promo.program.activated";
|
|
1006
|
+
readonly PROGRAM_PAUSED: "promo.program.paused";
|
|
1007
|
+
readonly PROGRAM_ARCHIVED: "promo.program.archived";
|
|
1008
|
+
readonly RULE_ADDED: "promo.rule.added";
|
|
1009
|
+
readonly RULE_UPDATED: "promo.rule.updated";
|
|
1010
|
+
readonly RULE_REMOVED: "promo.rule.removed";
|
|
1011
|
+
readonly REWARD_ADDED: "promo.reward.added";
|
|
1012
|
+
readonly REWARD_UPDATED: "promo.reward.updated";
|
|
1013
|
+
readonly REWARD_REMOVED: "promo.reward.removed";
|
|
1014
|
+
readonly VOUCHER_GENERATED: "promo.voucher.generated";
|
|
1015
|
+
readonly VOUCHER_REDEEMED: "promo.voucher.redeemed";
|
|
1016
|
+
readonly VOUCHER_CANCELLED: "promo.voucher.cancelled";
|
|
1017
|
+
readonly VOUCHER_EXPIRED: "promo.voucher.expired";
|
|
1018
|
+
readonly GIFT_CARD_SPENT: "promo.gift_card.spent";
|
|
1019
|
+
readonly GIFT_CARD_TOPPED_UP: "promo.gift_card.topped_up";
|
|
1020
|
+
readonly GIFT_CARD_EXHAUSTED: "promo.gift_card.exhausted";
|
|
1021
|
+
readonly EVALUATION_COMPLETED: "promo.evaluation.completed";
|
|
1022
|
+
readonly EVALUATION_COMMITTED: "promo.evaluation.committed";
|
|
1023
|
+
readonly EVALUATION_ROLLED_BACK: "promo.evaluation.rolled_back";
|
|
1024
|
+
};
|
|
1025
|
+
type PromoEventName = (typeof PromoEvents)[keyof typeof PromoEvents];
|
|
1026
|
+
//#endregion
|
|
1027
|
+
//#region src/events/in-process-bus.d.ts
|
|
1028
|
+
interface InProcessPromoBusOptions {
|
|
1029
|
+
logger?: {
|
|
1030
|
+
error(message: string, ...args: unknown[]): void;
|
|
1031
|
+
};
|
|
1032
|
+
}
|
|
1033
|
+
//#endregion
|
|
1034
|
+
//#region src/events/promo-event-catalog.d.ts
|
|
1035
|
+
interface PromoEventSchema {
|
|
1036
|
+
type: 'object';
|
|
1037
|
+
properties?: Record<string, {
|
|
1038
|
+
type?: string;
|
|
1039
|
+
format?: string;
|
|
1040
|
+
[key: string]: unknown;
|
|
1041
|
+
}>;
|
|
1042
|
+
required?: string[];
|
|
1043
|
+
[key: string]: unknown;
|
|
1044
|
+
}
|
|
1045
|
+
interface PromoEventDefinition<TSchema extends z.ZodType = z.ZodType> {
|
|
1046
|
+
readonly name: string;
|
|
1047
|
+
readonly version: number;
|
|
1048
|
+
readonly description?: string;
|
|
1049
|
+
readonly schema: PromoEventSchema;
|
|
1050
|
+
readonly zodSchema: TSchema;
|
|
1051
|
+
create(payload: z.infer<TSchema>, meta?: Partial<DomainEvent$1['meta']>): DomainEvent$1<z.infer<TSchema>>;
|
|
1052
|
+
readonly __payload?: z.infer<TSchema>;
|
|
1053
|
+
}
|
|
1054
|
+
type PromoEventPayloadOf<D> = D extends PromoEventDefinition<infer S> ? z.infer<S> : never;
|
|
1055
|
+
/** Mirrors `ProgramLifecyclePayload`. */
|
|
1056
|
+
declare const programLifecycleSchema: z.ZodObject<{
|
|
1057
|
+
programId: z.ZodString;
|
|
1058
|
+
programType: z.ZodString;
|
|
1059
|
+
status: z.ZodString;
|
|
1060
|
+
actorId: z.ZodOptional<z.ZodString>;
|
|
1061
|
+
}, z.core.$strip>;
|
|
1062
|
+
/** Mirrors `RulePayload`. */
|
|
1063
|
+
declare const ruleSchema: z.ZodObject<{
|
|
1064
|
+
programId: z.ZodString;
|
|
1065
|
+
ruleId: z.ZodString;
|
|
1066
|
+
actorId: z.ZodOptional<z.ZodString>;
|
|
1067
|
+
}, z.core.$strip>;
|
|
1068
|
+
/** Mirrors `RewardPayload`. */
|
|
1069
|
+
declare const rewardSchema: z.ZodObject<{
|
|
1070
|
+
programId: z.ZodString;
|
|
1071
|
+
rewardId: z.ZodString;
|
|
1072
|
+
actorId: z.ZodOptional<z.ZodString>;
|
|
1073
|
+
}, z.core.$strip>;
|
|
1074
|
+
/** Mirrors `VoucherGeneratedPayload`. */
|
|
1075
|
+
declare const voucherGeneratedSchema: z.ZodObject<{
|
|
1076
|
+
programId: z.ZodString;
|
|
1077
|
+
voucherIds: z.ZodArray<z.ZodString>;
|
|
1078
|
+
codes: z.ZodArray<z.ZodString>;
|
|
1079
|
+
count: z.ZodNumber;
|
|
1080
|
+
actorId: z.ZodOptional<z.ZodString>;
|
|
1081
|
+
}, z.core.$strip>;
|
|
1082
|
+
/** Mirrors `VoucherRedeemedPayload`. */
|
|
1083
|
+
declare const voucherRedeemedSchema: z.ZodObject<{
|
|
1084
|
+
voucherId: z.ZodString;
|
|
1085
|
+
code: z.ZodString;
|
|
1086
|
+
orderId: z.ZodString;
|
|
1087
|
+
discountAmount: z.ZodNumber;
|
|
1088
|
+
customerId: z.ZodOptional<z.ZodString>;
|
|
1089
|
+
}, z.core.$strip>;
|
|
1090
|
+
/**
|
|
1091
|
+
* Mirrors `VoucherLifecyclePayload` — shared by VOUCHER_CANCELLED,
|
|
1092
|
+
* VOUCHER_EXPIRED, and GIFT_CARD_EXHAUSTED (repo emits `status: 'cancelled'`
|
|
1093
|
+
* / `'used'` / host-supplied terminal value).
|
|
1094
|
+
*/
|
|
1095
|
+
declare const voucherLifecycleSchema: z.ZodObject<{
|
|
1096
|
+
voucherId: z.ZodString;
|
|
1097
|
+
code: z.ZodString;
|
|
1098
|
+
status: z.ZodString;
|
|
1099
|
+
}, z.core.$strip>;
|
|
1100
|
+
/** Mirrors `GiftCardSpentPayload`. */
|
|
1101
|
+
declare const giftCardSpentSchema: z.ZodObject<{
|
|
1102
|
+
voucherId: z.ZodString;
|
|
1103
|
+
code: z.ZodString;
|
|
1104
|
+
amount: z.ZodNumber;
|
|
1105
|
+
remainingBalance: z.ZodNumber;
|
|
1106
|
+
orderId: z.ZodString;
|
|
1107
|
+
}, z.core.$strip>;
|
|
1108
|
+
/** Mirrors `GiftCardToppedUpPayload`. */
|
|
1109
|
+
declare const giftCardToppedUpSchema: z.ZodObject<{
|
|
1110
|
+
voucherId: z.ZodString;
|
|
1111
|
+
code: z.ZodString;
|
|
1112
|
+
amount: z.ZodNumber;
|
|
1113
|
+
newBalance: z.ZodNumber;
|
|
1114
|
+
}, z.core.$strip>;
|
|
1115
|
+
/** Mirrors `EvaluationCompletedPayload`. */
|
|
1116
|
+
declare const evaluationCompletedSchema: z.ZodObject<{
|
|
1117
|
+
evaluationId: z.ZodString;
|
|
1118
|
+
totalDiscount: z.ZodNumber;
|
|
1119
|
+
programsApplied: z.ZodNumber;
|
|
1120
|
+
codesUsed: z.ZodArray<z.ZodString>;
|
|
1121
|
+
isPreview: z.ZodBoolean;
|
|
1122
|
+
}, z.core.$strip>;
|
|
1123
|
+
/** Mirrors `EvaluationCommittedPayload`. */
|
|
1124
|
+
declare const evaluationCommittedSchema: z.ZodObject<{
|
|
1125
|
+
evaluationId: z.ZodString;
|
|
1126
|
+
orderId: z.ZodString;
|
|
1127
|
+
totalDiscount: z.ZodNumber;
|
|
1128
|
+
}, z.core.$strip>;
|
|
1129
|
+
/** Single-field rollback payload — emitted by `evaluation.service.ts`. */
|
|
1130
|
+
declare const evaluationRolledBackSchema: z.ZodObject<{
|
|
1131
|
+
evaluationId: z.ZodString;
|
|
1132
|
+
}, z.core.$strip>;
|
|
1133
|
+
type ProgramLifecyclePayloadSchema = z.infer<typeof programLifecycleSchema>;
|
|
1134
|
+
type RulePayloadSchema = z.infer<typeof ruleSchema>;
|
|
1135
|
+
type RewardPayloadSchema = z.infer<typeof rewardSchema>;
|
|
1136
|
+
type VoucherGeneratedPayloadSchema = z.infer<typeof voucherGeneratedSchema>;
|
|
1137
|
+
type VoucherRedeemedPayloadSchema = z.infer<typeof voucherRedeemedSchema>;
|
|
1138
|
+
type VoucherLifecyclePayloadSchema = z.infer<typeof voucherLifecycleSchema>;
|
|
1139
|
+
type GiftCardSpentPayloadSchema = z.infer<typeof giftCardSpentSchema>;
|
|
1140
|
+
type GiftCardToppedUpPayloadSchema = z.infer<typeof giftCardToppedUpSchema>;
|
|
1141
|
+
type EvaluationCompletedPayloadSchema = z.infer<typeof evaluationCompletedSchema>;
|
|
1142
|
+
type EvaluationCommittedPayloadSchema = z.infer<typeof evaluationCommittedSchema>;
|
|
1143
|
+
type EvaluationRolledBackPayloadSchema = z.infer<typeof evaluationRolledBackSchema>;
|
|
1144
|
+
declare const ProgramCreated: PromoEventDefinition<z.ZodObject<{
|
|
1145
|
+
programId: z.ZodString;
|
|
1146
|
+
programType: z.ZodString;
|
|
1147
|
+
status: z.ZodString;
|
|
1148
|
+
actorId: z.ZodOptional<z.ZodString>;
|
|
1149
|
+
}, z.core.$strip>>;
|
|
1150
|
+
declare const ProgramActivated: PromoEventDefinition<z.ZodObject<{
|
|
1151
|
+
programId: z.ZodString;
|
|
1152
|
+
programType: z.ZodString;
|
|
1153
|
+
status: z.ZodString;
|
|
1154
|
+
actorId: z.ZodOptional<z.ZodString>;
|
|
1155
|
+
}, z.core.$strip>>;
|
|
1156
|
+
declare const ProgramPaused: PromoEventDefinition<z.ZodObject<{
|
|
1157
|
+
programId: z.ZodString;
|
|
1158
|
+
programType: z.ZodString;
|
|
1159
|
+
status: z.ZodString;
|
|
1160
|
+
actorId: z.ZodOptional<z.ZodString>;
|
|
1161
|
+
}, z.core.$strip>>;
|
|
1162
|
+
declare const ProgramArchived: PromoEventDefinition<z.ZodObject<{
|
|
1163
|
+
programId: z.ZodString;
|
|
1164
|
+
programType: z.ZodString;
|
|
1165
|
+
status: z.ZodString;
|
|
1166
|
+
actorId: z.ZodOptional<z.ZodString>;
|
|
1167
|
+
}, z.core.$strip>>;
|
|
1168
|
+
declare const RuleAdded: PromoEventDefinition<z.ZodObject<{
|
|
1169
|
+
programId: z.ZodString;
|
|
1170
|
+
ruleId: z.ZodString;
|
|
1171
|
+
actorId: z.ZodOptional<z.ZodString>;
|
|
1172
|
+
}, z.core.$strip>>;
|
|
1173
|
+
declare const RuleUpdated: PromoEventDefinition<z.ZodObject<{
|
|
1174
|
+
programId: z.ZodString;
|
|
1175
|
+
ruleId: z.ZodString;
|
|
1176
|
+
actorId: z.ZodOptional<z.ZodString>;
|
|
1177
|
+
}, z.core.$strip>>;
|
|
1178
|
+
declare const RuleRemoved: PromoEventDefinition<z.ZodObject<{
|
|
1179
|
+
programId: z.ZodString;
|
|
1180
|
+
ruleId: z.ZodString;
|
|
1181
|
+
actorId: z.ZodOptional<z.ZodString>;
|
|
1182
|
+
}, z.core.$strip>>;
|
|
1183
|
+
declare const RewardAdded: PromoEventDefinition<z.ZodObject<{
|
|
1184
|
+
programId: z.ZodString;
|
|
1185
|
+
rewardId: z.ZodString;
|
|
1186
|
+
actorId: z.ZodOptional<z.ZodString>;
|
|
1187
|
+
}, z.core.$strip>>;
|
|
1188
|
+
declare const RewardUpdated: PromoEventDefinition<z.ZodObject<{
|
|
1189
|
+
programId: z.ZodString;
|
|
1190
|
+
rewardId: z.ZodString;
|
|
1191
|
+
actorId: z.ZodOptional<z.ZodString>;
|
|
1192
|
+
}, z.core.$strip>>;
|
|
1193
|
+
declare const RewardRemoved: PromoEventDefinition<z.ZodObject<{
|
|
1194
|
+
programId: z.ZodString;
|
|
1195
|
+
rewardId: z.ZodString;
|
|
1196
|
+
actorId: z.ZodOptional<z.ZodString>;
|
|
1197
|
+
}, z.core.$strip>>;
|
|
1198
|
+
declare const VoucherGenerated: PromoEventDefinition<z.ZodObject<{
|
|
1199
|
+
programId: z.ZodString;
|
|
1200
|
+
voucherIds: z.ZodArray<z.ZodString>;
|
|
1201
|
+
codes: z.ZodArray<z.ZodString>;
|
|
1202
|
+
count: z.ZodNumber;
|
|
1203
|
+
actorId: z.ZodOptional<z.ZodString>;
|
|
1204
|
+
}, z.core.$strip>>;
|
|
1205
|
+
declare const VoucherRedeemed: PromoEventDefinition<z.ZodObject<{
|
|
1206
|
+
voucherId: z.ZodString;
|
|
1207
|
+
code: z.ZodString;
|
|
1208
|
+
orderId: z.ZodString;
|
|
1209
|
+
discountAmount: z.ZodNumber;
|
|
1210
|
+
customerId: z.ZodOptional<z.ZodString>;
|
|
1211
|
+
}, z.core.$strip>>;
|
|
1212
|
+
declare const VoucherCancelled: PromoEventDefinition<z.ZodObject<{
|
|
1213
|
+
voucherId: z.ZodString;
|
|
1214
|
+
code: z.ZodString;
|
|
1215
|
+
status: z.ZodString;
|
|
1216
|
+
}, z.core.$strip>>;
|
|
1217
|
+
declare const VoucherExpired: PromoEventDefinition<z.ZodObject<{
|
|
1218
|
+
voucherId: z.ZodString;
|
|
1219
|
+
code: z.ZodString;
|
|
1220
|
+
status: z.ZodString;
|
|
1221
|
+
}, z.core.$strip>>;
|
|
1222
|
+
declare const GiftCardSpent: PromoEventDefinition<z.ZodObject<{
|
|
1223
|
+
voucherId: z.ZodString;
|
|
1224
|
+
code: z.ZodString;
|
|
1225
|
+
amount: z.ZodNumber;
|
|
1226
|
+
remainingBalance: z.ZodNumber;
|
|
1227
|
+
orderId: z.ZodString;
|
|
1228
|
+
}, z.core.$strip>>;
|
|
1229
|
+
declare const GiftCardToppedUp: PromoEventDefinition<z.ZodObject<{
|
|
1230
|
+
voucherId: z.ZodString;
|
|
1231
|
+
code: z.ZodString;
|
|
1232
|
+
amount: z.ZodNumber;
|
|
1233
|
+
newBalance: z.ZodNumber;
|
|
1234
|
+
}, z.core.$strip>>;
|
|
1235
|
+
declare const GiftCardExhausted: PromoEventDefinition<z.ZodObject<{
|
|
1236
|
+
voucherId: z.ZodString;
|
|
1237
|
+
code: z.ZodString;
|
|
1238
|
+
status: z.ZodString;
|
|
1239
|
+
}, z.core.$strip>>;
|
|
1240
|
+
declare const EvaluationCompleted: PromoEventDefinition<z.ZodObject<{
|
|
1241
|
+
evaluationId: z.ZodString;
|
|
1242
|
+
totalDiscount: z.ZodNumber;
|
|
1243
|
+
programsApplied: z.ZodNumber;
|
|
1244
|
+
codesUsed: z.ZodArray<z.ZodString>;
|
|
1245
|
+
isPreview: z.ZodBoolean;
|
|
1246
|
+
}, z.core.$strip>>;
|
|
1247
|
+
declare const EvaluationCommitted: PromoEventDefinition<z.ZodObject<{
|
|
1248
|
+
evaluationId: z.ZodString;
|
|
1249
|
+
orderId: z.ZodString;
|
|
1250
|
+
totalDiscount: z.ZodNumber;
|
|
1251
|
+
}, z.core.$strip>>;
|
|
1252
|
+
declare const EvaluationRolledBack: PromoEventDefinition<z.ZodObject<{
|
|
1253
|
+
evaluationId: z.ZodString;
|
|
1254
|
+
}, z.core.$strip>>;
|
|
1255
|
+
/**
|
|
1256
|
+
* Every promo event defined in the package — pass to Arc's
|
|
1257
|
+
* `EventRegistry`. Hosts wire ONE array; the whole `promo.*` namespace
|
|
1258
|
+
* becomes introspectable via OpenAPI and auto-validated at publish time
|
|
1259
|
+
* when `eventPlugin({ validateMode: 'reject' })` is set.
|
|
1260
|
+
*/
|
|
1261
|
+
declare const promoEventDefinitions: ReadonlyArray<PromoEventDefinition>;
|
|
1262
|
+
//#endregion
|
|
10
1263
|
//#region src/index.d.ts
|
|
11
1264
|
declare function resolveConfig(config: PromoConfig): ResolvedConfig;
|
|
12
1265
|
interface PromoEngine {
|
|
13
1266
|
models: PromoModels;
|
|
14
1267
|
repositories: PromoRepositories;
|
|
15
1268
|
services: PromoServices;
|
|
16
|
-
events:
|
|
1269
|
+
events: EventTransport$1;
|
|
1270
|
+
syncIndexes(): Promise<void>;
|
|
17
1271
|
}
|
|
1272
|
+
/**
|
|
1273
|
+
* Build the promo engine for a host application.
|
|
1274
|
+
*
|
|
1275
|
+
* **Index management — important for boot performance:**
|
|
1276
|
+
*
|
|
1277
|
+
* `createPromoEngine` itself is non-blocking. It registers Mongoose models
|
|
1278
|
+
* and returns immediately. Index creation is delegated to Mongoose's
|
|
1279
|
+
* standard lazy-init: with `autoIndex: true` (default in dev) Mongoose
|
|
1280
|
+
* builds indexes in the background after the first query touches each
|
|
1281
|
+
* model; with `autoIndex: false` (recommended for production) hosts
|
|
1282
|
+
* control index creation explicitly.
|
|
1283
|
+
*
|
|
1284
|
+
* The returned `engine.syncIndexes()` helper is opt-in and **MUST NOT be
|
|
1285
|
+
* `await`ed during Fastify plugin registration / boot** — Atlas index
|
|
1286
|
+
* creation can take 10s+ on fresh collections, longer than typical
|
|
1287
|
+
* plugin timeouts. Three safe patterns:
|
|
1288
|
+
*
|
|
1289
|
+
* 1. **Migration script** (recommended for production):
|
|
1290
|
+
* ```ts
|
|
1291
|
+
* // scripts/sync-indexes.ts
|
|
1292
|
+
* await engine.syncIndexes();
|
|
1293
|
+
* ```
|
|
1294
|
+
* Run before deploying / serving traffic.
|
|
1295
|
+
*
|
|
1296
|
+
* 2. **Background fire-and-log** during boot:
|
|
1297
|
+
* ```ts
|
|
1298
|
+
* engine.syncIndexes().catch((err) => log.warn({ err }, 'index sync'));
|
|
1299
|
+
* ```
|
|
1300
|
+
* App accepts traffic immediately; first queries may wait briefly
|
|
1301
|
+
* on still-building indexes.
|
|
1302
|
+
*
|
|
1303
|
+
* 3. **Lazy init** (`autoIndex: true` in dev): just don't call
|
|
1304
|
+
* `syncIndexes()` at all. Mongoose schedules creation on first
|
|
1305
|
+
* query per model.
|
|
1306
|
+
*
|
|
1307
|
+
* Production hosts should set `autoIndex: false` and use option (1).
|
|
1308
|
+
*/
|
|
18
1309
|
declare function createPromoEngine(config: PromoConfig): PromoEngine;
|
|
19
1310
|
//#endregion
|
|
20
|
-
export { type BalanceLedgerEntry, type CartItem, type CommitResult, type CreateProgramInput, type CreateRewardInput, type CreateRuleInput, type DiscountLine, type EvaluateInput, type EvaluationResult, type
|
|
1311
|
+
export { type BalanceLedgerEntry, CartHashMismatchError, type CartItem, type CommitOptions, type CommitResult, ConcurrencyConflictError, type CreateProgramInput, type CreateRewardInput, type CreateRuleInput, type DiscountLine, type DomainEvent, DuplicateRedemptionError, type EvaluateInput, EvaluationCommitted, type EvaluationCommittedPayloadSchema, EvaluationCompleted, type EvaluationCompletedPayloadSchema, EvaluationNotFoundError, type EvaluationResult, EvaluationRolledBack, type EvaluationRolledBackPayloadSchema, type EvaluationStore, type EvaluationStoreContext, type EventHandler, type EventTransport, type FreeProductLine, type GenerateCodesInput, type GenerateSingleCodeInput, type GiftCardBalance, GiftCardExhausted, GiftCardExhaustedError, type GiftCardSpendInput, GiftCardSpent, type GiftCardSpentPayloadSchema, type GiftCardTopUpInput, GiftCardToppedUp, type GiftCardToppedUpPayloadSchema, InMemoryEvaluationStore, type InProcessPromoBusOptions, InsufficientBalanceError, InvalidTransitionError, type ListQuery, MongoEvaluationStore, type PaginatedResult, type PendingEvaluationDocument, PendingEvaluationRepository, type PluginType, type Program, ProgramActivated, ProgramArchived, ProgramCreated, type ProgramLifecyclePayloadSchema, ProgramNotFoundError, ProgramPaused, ProgramRepository, ProgramUsageCapExceededError, type PromoConfig, type PromoContext, PromoEngine, PromoError, type PromoEventDefinition, type PromoEventName, type PromoEventPayloadOf, type PromoEventSchema, PromoEvents, type PromoModels, type PromoRepositories, type PromoServices, type RedeemVoucherInput, type RejectedCode, type RepositoryDispatchDeps, type RepositoryPluginsMap, type ResolvedConfig, type ResolvedTenant, type Reward, RewardAdded, RewardNotFoundError, type RewardPayloadSchema, RewardRemoved, RewardRepository, RewardUpdated, type Rule, RuleAdded, RuleNotFoundError, type RulePayloadSchema, RuleRemoved, RuleRepository, RuleUpdated, type StoredEvaluationSnapshot, type TenantFieldType, TenantIsolationError, type UpdateProgramInput, type UpdateRewardInput, type UpdateRuleInput, ValidationError, type Voucher, VoucherCancelled, VoucherExhaustedError, VoucherExpired, VoucherExpiredError, VoucherGenerated, type VoucherGeneratedPayloadSchema, type VoucherLifecyclePayloadSchema, VoucherNotFoundError, VoucherRedeemed, type VoucherRedeemedPayloadSchema, type VoucherRedemption, VoucherRepository, type VoucherValidation, createPromoEngine, promoEventDefinitions, resolveConfig };
|