@classytic/promo 0.2.0 → 0.2.2

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/dist/index.d.mts CHANGED
@@ -1,91 +1,12 @@
1
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 { ResolvedTenantConfig, TenantConfig } from "@classytic/primitives/tenant";
2
+ import { PluginType as PluginType$1, Repository } from "@classytic/mongokit";
3
+ import { ResolvedTenantConfig, TenantConfig, TenantFieldType } from "@classytic/primitives/tenant";
3
4
  import { DomainEvent, DomainEvent as DomainEvent$1, EventHandler, EventTransport, EventTransport as EventTransport$1 } from "@classytic/primitives/events";
4
5
  import { ClientSession, Connection, Model } from "mongoose";
5
- import { PluginType, Repository } from "@classytic/mongokit";
6
6
  import { z } from "zod";
7
7
  import { OutboxStore } from "@classytic/primitives/outbox";
8
8
  import { OperationContext } from "@classytic/primitives/context";
9
9
 
10
- //#region src/types/config.d.ts
11
- type TenantConfig$1 = false | TenantConfig;
12
- interface IndexDefinition {
13
- fields: Record<string, 1 | -1>;
14
- options?: Record<string, unknown> | undefined;
15
- }
16
- type ModelName = 'program' | 'rule' | 'reward' | 'voucher';
17
- interface RepositoryPlugins {
18
- program?: PluginType[] | undefined;
19
- rule?: PluginType[] | undefined;
20
- reward?: PluginType[] | undefined;
21
- voucher?: PluginType[] | undefined;
22
- }
23
- interface PromoConfig {
24
- mongoose: Connection;
25
- tenant?: TenantConfig$1 | undefined;
26
- evaluation?: {
27
- maxStackablePromotions?: number | undefined;
28
- allowExclusiveAndStackable?: boolean | undefined;
29
- } | undefined;
30
- voucher?: {
31
- codeLength?: number | undefined;
32
- codePrefix?: string | undefined;
33
- defaultExpiryDays?: number | undefined;
34
- } | undefined;
35
- giftCard?: {
36
- allowNegativeBalance?: boolean | undefined;
37
- maxBalance?: number | undefined;
38
- } | undefined;
39
- indexes?: Partial<Record<ModelName, IndexDefinition[]>> | undefined;
40
- autoIndex?: boolean | Partial<Record<ModelName, boolean>> | undefined;
41
- events?: {
42
- transport?: EventTransport$1 | undefined;
43
- } | undefined;
44
- /**
45
- * Host-provided outbox store (PACKAGE_RULES §5.5 + §P8). When wired, every
46
- * domain event is persisted to the outbox inside the caller's `ctx.session`
47
- * BEFORE being published to the transport — giving at-least-once delivery
48
- * via the host-side relay. Absent → transport-only best-effort.
49
- *
50
- * Explicit `| undefined` per `exactOptionalPropertyTypes` convention.
51
- */
52
- outbox?: OutboxStore | undefined;
53
- /**
54
- * Optional logger for dispatch-layer errors. Used by the shared
55
- * `dispatch()` helper to surface outbox/transport failures without
56
- * aborting the domain mutation. Defaults to `console`.
57
- */
58
- logger?: {
59
- error(message: string, ...args: unknown[]): void;
60
- } | undefined;
61
- plugins?: RepositoryPlugins | undefined;
62
- }
63
- type ResolvedTenant = ResolvedTenantConfig;
64
- interface ResolvedConfig {
65
- evaluation: {
66
- maxStackablePromotions: number;
67
- allowExclusiveAndStackable: boolean;
68
- };
69
- voucher: {
70
- codeLength: number;
71
- codePrefix: string;
72
- defaultExpiryDays: number | null;
73
- };
74
- giftCard: {
75
- allowNegativeBalance: boolean;
76
- maxBalance: number | null;
77
- };
78
- tenant: ResolvedTenant;
79
- }
80
- //#endregion
81
- //#region src/models/create-models.d.ts
82
- interface PromoModels {
83
- Program: Model<unknown>;
84
- Rule: Model<unknown>;
85
- Reward: Model<unknown>;
86
- Voucher: Model<unknown>;
87
- }
88
- //#endregion
89
10
  //#region src/types/inputs.d.ts
90
11
  /**
91
12
  * Extends `@classytic/primitives`' {@link OperationContext}. The
@@ -239,6 +160,295 @@ interface ListQuery {
239
160
  filters?: Record<string, unknown>;
240
161
  }
241
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
242
452
  //#region src/events/dispatch.d.ts
243
453
  interface DispatchLogger {
244
454
  error(message: string, ...args: unknown[]): void;
@@ -249,6 +459,92 @@ interface DispatchDeps {
249
459
  logger?: DispatchLogger | undefined;
250
460
  }
251
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
252
548
  //#region src/domain/entities/program.d.ts
253
549
  interface Program {
254
550
  _id: string;
@@ -275,12 +571,29 @@ interface Program {
275
571
  //#region src/repositories/program.repository.d.ts
276
572
  declare class ProgramRepository extends Repository<Program> {
277
573
  private dispatchDeps;
278
- constructor(model: Model<Program>, plugins?: PluginType[], dispatchDeps?: DispatchDeps);
574
+ constructor(model: Model<Program>, plugins?: PluginType$1[], dispatchDeps?: DispatchDeps);
279
575
  activate(id: string, ctx: PromoContext): Promise<Program>;
280
576
  pause(id: string, ctx: PromoContext): Promise<Program>;
281
577
  archive(id: string, ctx: PromoContext): Promise<Program>;
282
578
  private _transition;
283
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>;
284
597
  decrementUsage(id: string, ctx?: Record<string, unknown>): Promise<Program>;
285
598
  getCustomerUsage(id: string, customerId: string, ctx?: Record<string, unknown>): Promise<number>;
286
599
  incrementCustomerUsage(id: string, customerId: string, ctx?: Record<string, unknown>): Promise<Program>;
@@ -309,7 +622,7 @@ interface Reward {
309
622
  //#endregion
310
623
  //#region src/repositories/reward.repository.d.ts
311
624
  declare class RewardRepository extends Repository<Reward> {
312
- constructor(model: Model<Reward>, plugins?: PluginType[]);
625
+ constructor(model: Model<Reward>, plugins?: PluginType$1[]);
313
626
  }
314
627
  //#endregion
315
628
  //#region src/domain/entities/rule.d.ts
@@ -333,7 +646,7 @@ interface Rule {
333
646
  //#endregion
334
647
  //#region src/repositories/rule.repository.d.ts
335
648
  declare class RuleRepository extends Repository<Rule> {
336
- constructor(model: Model<Rule>, plugins?: PluginType[]);
649
+ constructor(model: Model<Rule>, plugins?: PluginType$1[]);
337
650
  }
338
651
  //#endregion
339
652
  //#region src/domain/entities/voucher.d.ts
@@ -343,6 +656,12 @@ interface BalanceLedgerEntry {
343
656
  description?: string;
344
657
  createdAt: Date;
345
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;
346
665
  }
347
666
  interface VoucherRedemption {
348
667
  orderId: string;
@@ -350,6 +669,9 @@ interface VoucherRedemption {
350
669
  discountAmount: number;
351
670
  redeemedAt: Date;
352
671
  idempotencyKey?: string;
672
+ /** Branch / store / location where the redemption happened. Stamped
673
+ * from `ctx.organizationId` at write time. See `BalanceLedgerEntry`. */
674
+ organizationId?: string;
353
675
  }
354
676
  interface Voucher {
355
677
  _id: string;
@@ -373,14 +695,19 @@ interface Voucher {
373
695
  declare class VoucherRepository extends Repository<Voucher> {
374
696
  private dispatchDeps;
375
697
  private tenantField;
376
- constructor(model: Model<Voucher>, plugins?: PluginType[], dispatchDeps?: DispatchDeps, tenantField?: string);
698
+ private tenantEnabled;
699
+ constructor(model: Model<Voucher>, plugins?: PluginType$1[], dispatchDeps?: DispatchDeps, tenantField?: string, tenantEnabled?: boolean);
377
700
  /**
378
701
  * Copy the tenant id from `ctx` onto the write payload so the doc persists
379
702
  * with the correct `organizationId` even when the host has opted OUT of the
380
- * auto-wired `multiTenantPlugin` (e.g. arc hosts that scope at the
381
- * framework layer — see `@classytic/order` CLAUDE.md: "Child repos set
382
- * `organizationId` explicitly on the doc"). No-op when `ctx` omits the
383
- * tenant id or the payload already carries it.
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.
384
711
  */
385
712
  private _injectTenant;
386
713
  create(data: Parameters<Repository<Voucher>['create']>[0], options?: Parameters<Repository<Voucher>['create']>[1]): Promise<Voucher>;
@@ -416,6 +743,10 @@ declare class VoucherRepository extends Repository<Voucher> {
416
743
  * (e.g. arc's preset + `BaseController`). Without this, a host that
417
744
  * runs the plugin off would leak vouchers across branches at
418
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.
419
750
  */
420
751
  getByCode(code: string, ctx?: Record<string, unknown>): Promise<Voucher | null>;
421
752
  /**
@@ -432,6 +763,19 @@ interface PromoRepositories {
432
763
  rule: RuleRepository;
433
764
  reward: RewardRepository;
434
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;
435
779
  }
436
780
  //#endregion
437
781
  //#region src/domain/ports/unit-of-work.port.d.ts
@@ -440,88 +784,6 @@ interface UnitOfWork {
440
784
  withTransaction<T>(cb: (session: TransactionSession) => Promise<T>): Promise<T>;
441
785
  }
442
786
  //#endregion
443
- //#region src/types/results.d.ts
444
- interface DiscountLine {
445
- programId: string;
446
- programName: string;
447
- rewardId: string;
448
- type: 'percentage' | 'fixed';
449
- scope: 'order' | 'cheapest' | 'specific_products';
450
- amount: number;
451
- description: string;
452
- voucherCode?: string;
453
- }
454
- interface FreeProductLine {
455
- programId: string;
456
- programName: string;
457
- rewardId: string;
458
- productId?: string;
459
- productSku?: string;
460
- quantity: number;
461
- description: string;
462
- }
463
- interface RejectedCode {
464
- code: string;
465
- reason: string;
466
- }
467
- interface EvaluationResult {
468
- evaluationId: string;
469
- /**
470
- * Deterministic hash of the evaluated cart (items + subtotal + codes +
471
- * customerId). Hosts that want tamper-proof commits pass this hash back
472
- * to `EvaluationService.commit(evaluationId, orderId, ctx, { cartHash })`;
473
- * the engine throws `CartHashMismatchError` when the caller's hash does
474
- * not match what was evaluated. Omitting the hash at commit keeps the
475
- * legacy "trusted host" behaviour.
476
- */
477
- cartHash: string;
478
- appliedDiscounts: DiscountLine[];
479
- freeProducts: FreeProductLine[];
480
- totalDiscount: number;
481
- subtotalAfterDiscount: number;
482
- appliedCodes: string[];
483
- rejectedCodes: RejectedCode[];
484
- warnings: string[];
485
- isPreview: boolean;
486
- programsApplied: string[];
487
- }
488
- interface CommitResult {
489
- evaluationId: string;
490
- orderId: string;
491
- totalDiscount: number;
492
- programsCommitted: number;
493
- vouchersUsed: number;
494
- }
495
- interface VoucherValidation {
496
- valid: boolean;
497
- voucher?: {
498
- code: string;
499
- programId: string;
500
- programType: string;
501
- status: string;
502
- remainingUses: number;
503
- currentBalance?: number;
504
- expiresAt?: Date;
505
- };
506
- error?: string;
507
- }
508
- interface GiftCardBalance {
509
- code: string;
510
- initialBalance: number;
511
- currentBalance: number;
512
- spent: number;
513
- voucherId: string;
514
- }
515
- interface PaginatedResult<T> {
516
- docs: T[];
517
- page: number;
518
- limit: number;
519
- total: number;
520
- pages: number;
521
- hasNext: boolean;
522
- hasPrev: boolean;
523
- }
524
- //#endregion
525
787
  //#region src/services/evaluation.service.d.ts
526
788
  interface CommitOptions {
527
789
  /**
@@ -543,11 +805,12 @@ declare class EvaluationService {
543
805
  private unitOfWork;
544
806
  private dispatchDeps;
545
807
  private config;
546
- private pendingEvaluations;
547
- constructor(programRepo: ProgramRepository, ruleRepo: RuleRepository, rewardRepo: RewardRepository, voucherRepo: VoucherRepository, unitOfWork: UnitOfWork, dispatchDeps: DispatchDeps, config: ResolvedConfig);
808
+ private store;
809
+ constructor(programRepo: ProgramRepository, ruleRepo: RuleRepository, rewardRepo: RewardRepository, voucherRepo: VoucherRepository, unitOfWork: UnitOfWork, dispatchDeps: DispatchDeps, config: ResolvedConfig, store: EvaluationStore);
548
810
  evaluate(input: EvaluateInput, ctx: PromoContext): Promise<EvaluationResult>;
549
811
  preview(input: EvaluateInput, ctx: PromoContext): Promise<EvaluationResult>;
550
812
  commit(evaluationId: string, orderId: string, ctx: PromoContext, options?: CommitOptions): Promise<CommitResult>;
813
+ private commitInTransaction;
551
814
  rollback(evaluationId: string, ctx: PromoContext): Promise<void>;
552
815
  private doEvaluate;
553
816
  private isCustomerEligible;
@@ -585,6 +848,7 @@ declare class VoucherService {
585
848
  spend(input: GiftCardSpendInput, ctx: PromoContext): Promise<GiftCardBalance>;
586
849
  private spendInTransaction;
587
850
  topUp(input: GiftCardTopUpInput, ctx: PromoContext): Promise<GiftCardBalance>;
851
+ private topUpInTransaction;
588
852
  private assertVoucherUsable;
589
853
  }
590
854
  //#endregion
@@ -594,6 +858,147 @@ interface PromoServices {
594
858
  evaluation: EvaluationService;
595
859
  }
596
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
597
1002
  //#region src/events/event-constants.d.ts
598
1003
  declare const PromoEvents: {
599
1004
  readonly PROGRAM_CREATED: "promo.program.created";
@@ -619,6 +1024,13 @@ declare const PromoEvents: {
619
1024
  };
620
1025
  type PromoEventName = (typeof PromoEvents)[keyof typeof PromoEvents];
621
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
622
1034
  //#region src/events/promo-event-catalog.d.ts
623
1035
  interface PromoEventSchema {
624
1036
  type: 'object';
@@ -640,6 +1052,95 @@ interface PromoEventDefinition<TSchema extends z.ZodType = z.ZodType> {
640
1052
  readonly __payload?: z.infer<TSchema>;
641
1053
  }
642
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>;
643
1144
  declare const ProgramCreated: PromoEventDefinition<z.ZodObject<{
644
1145
  programId: z.ZodString;
645
1146
  programType: z.ZodString;
@@ -768,6 +1269,43 @@ interface PromoEngine {
768
1269
  events: EventTransport$1;
769
1270
  syncIndexes(): Promise<void>;
770
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
+ */
771
1309
  declare function createPromoEngine(config: PromoConfig): PromoEngine;
772
1310
  //#endregion
773
- export { type BalanceLedgerEntry, type CartItem, type CommitResult, type CreateProgramInput, type CreateRewardInput, type CreateRuleInput, type DiscountLine, type DomainEvent, type EvaluateInput, EvaluationCommitted, EvaluationCompleted, type EvaluationResult, EvaluationRolledBack, type EventHandler, type EventTransport, type FreeProductLine, type GenerateCodesInput, type GenerateSingleCodeInput, type GiftCardBalance, GiftCardExhausted, type GiftCardSpendInput, GiftCardSpent, type GiftCardTopUpInput, GiftCardToppedUp, type ListQuery, type PaginatedResult, type Program, ProgramActivated, ProgramArchived, ProgramCreated, ProgramPaused, ProgramRepository, type PromoConfig, type PromoContext, PromoEngine, type PromoEventDefinition, type PromoEventName, type PromoEventPayloadOf, type PromoEventSchema, PromoEvents, type PromoModels, type PromoRepositories, type PromoServices, type RedeemVoucherInput, type RejectedCode, type ResolvedConfig, type ResolvedTenant, type Reward, RewardAdded, RewardRemoved, RewardRepository, RewardUpdated, type Rule, RuleAdded, RuleRemoved, RuleRepository, RuleUpdated, type UpdateProgramInput, type UpdateRewardInput, type UpdateRuleInput, type Voucher, VoucherCancelled, VoucherExpired, VoucherGenerated, VoucherRedeemed, type VoucherRedemption, VoucherRepository, type VoucherValidation, createPromoEngine, promoEventDefinitions, resolveConfig };
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 };