@classytic/promo 0.2.5 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -1,10 +1,11 @@
1
- import { C as VoucherExhaustedError, E as PromoError, S as ValidationError, T as VoucherNotFoundError, _ as ProgramNotFoundError, a as PROGRAM_TYPES, b as RuleNotFoundError, c as TRIGGER_MODES, d as ConcurrencyConflictError, f as DuplicateRedemptionError, g as InvalidTransitionError, h as InsufficientBalanceError, i as PROGRAM_STATUSES, l as VOUCHER_STATUSES, m as GiftCardExhaustedError, n as DISCOUNT_SCOPES, o as REWARD_TYPES, p as EvaluationNotFoundError, r as PROGRAM_MACHINE, s as STACKING_MODES, t as DISCOUNT_MODES, u as CartHashMismatchError, v as ProgramUsageCapExceededError, w as VoucherExpiredError, x as TenantIsolationError, y as RewardNotFoundError } from "./constants-BB5O8zlN.mjs";
2
- import { Repository, multiTenantPlugin, withTransaction } from "@classytic/mongokit";
1
+ import { C as TenantIsolationError, D as VoucherNotFoundError, E as VoucherExpiredError, O as PromoError, S as RuleNotFoundError, T as VoucherExhaustedError, _ as InvalidTransitionError, a as PROGRAM_TYPES, b as PromoModelCollisionError, c as TRIGGER_MODES, d as ConcurrencyConflictError, f as DuplicateRedemptionError, g as InsufficientBalanceError, h as GiftCardExhaustedError, i as PROGRAM_STATUSES, l as VOUCHER_STATUSES, m as EvaluationNotFoundError, n as DISCOUNT_SCOPES, o as REWARD_TYPES, p as DuplicateVoucherCodeError, r as PROGRAM_MACHINE, s as STACKING_MODES, t as DISCOUNT_MODES, u as CartHashMismatchError, v as ProgramNotFoundError, w as ValidationError, x as RewardNotFoundError, y as ProgramUsageCapExceededError } from "./constants-BE886vJk.mjs";
2
+ import { S as PromoEvents, _ as VoucherCancelled, a as GiftCardSpent, b as VoucherRedeemed, c as ProgramArchived, d as RewardAdded, f as RewardRemoved, g as RuleUpdated, h as RuleRemoved, i as GiftCardExhausted, l as ProgramCreated, m as RuleAdded, n as EvaluationCompleted, o as GiftCardToppedUp, p as RewardUpdated, r as EvaluationRolledBack, s as ProgramActivated, t as EvaluationCommitted, u as ProgramPaused, v as VoucherExpired, x as promoEventDefinitions, y as VoucherGenerated } from "./promo-event-catalog-Dyh0xQ6w.mjs";
3
+ import { Repository, isDuplicateKeyError, multiTenantPlugin, withTransaction } from "@classytic/mongokit";
3
4
  import { resolveTenantConfig } from "@classytic/repo-core/tenant";
4
5
  import { createEvent, matchEventPattern } from "@classytic/primitives/events";
5
6
  import mongoose, { Schema } from "mongoose";
7
+ import { throwIfAborted } from "@classytic/repo-core/repository";
6
8
  import { createHash, randomBytes } from "node:crypto";
7
- import { z } from "zod";
8
9
  //#region src/events/in-process-bus.ts
9
10
  var InProcessPromoBus = class {
10
11
  name = "in-process-promo";
@@ -367,6 +368,21 @@ const MODEL_NAMES = [
367
368
  "PromoVoucher",
368
369
  "PromoPendingEvaluation"
369
370
  ];
371
+ /**
372
+ * Explicit physical collection names (PACKAGE_RULES §20.1) — the values
373
+ * Mongoose's pluralizer produced before they were pinned, so existing
374
+ * deployments keep their data with zero migration. Passed as the third
375
+ * arg of `connection.model()` so a host that disables/changes the
376
+ * pluralizer can't silently rename collections. Changing any entry is a
377
+ * BREAKING change (renames the physical collection — ship a migration).
378
+ */
379
+ const DEFAULT_COLLECTIONS = {
380
+ PromoProgram: "promoprograms",
381
+ PromoRule: "promorules",
382
+ PromoReward: "promorewards",
383
+ PromoVoucher: "promovouchers",
384
+ PromoPendingEvaluation: "promopendingevaluations"
385
+ };
370
386
  function applyAutoIndex(models, autoIndex) {
371
387
  if (autoIndex === void 0) return;
372
388
  for (const [configKey, modelKey] of [
@@ -379,8 +395,11 @@ function applyAutoIndex(models, autoIndex) {
379
395
  if (value !== void 0) models[modelKey].schema.set("autoIndex", value);
380
396
  }
381
397
  }
382
- function createModels(connection, tenant, indexes, autoIndex) {
383
- for (const name of MODEL_NAMES) if (connection.models[name]) connection.deleteModel(name);
398
+ function createModels(connection, tenant, options = {}) {
399
+ const { indexes, autoIndex } = options;
400
+ if (options.forceRecreate) {
401
+ for (const name of MODEL_NAMES) if (connection.models[name]) connection.deleteModel(name);
402
+ } else for (const name of MODEL_NAMES) if (connection.models[name]) throw new PromoModelCollisionError(name);
384
403
  const programSchema = createProgramSchema();
385
404
  const ruleSchema = createRuleSchema();
386
405
  const rewardSchema = createRewardSchema();
@@ -396,12 +415,13 @@ function createModels(connection, tenant, indexes, autoIndex) {
396
415
  if (indexes?.rule) applyUserIndexes(ruleSchema, indexes.rule, tenant);
397
416
  if (indexes?.reward) applyUserIndexes(rewardSchema, indexes.reward, tenant);
398
417
  if (indexes?.voucher) applyUserIndexes(voucherSchema, indexes.voucher, tenant);
418
+ const prefix = options.collectionPrefix ?? "";
399
419
  const result = {
400
- Program: connection.model("PromoProgram", programSchema),
401
- Rule: connection.model("PromoRule", ruleSchema),
402
- Reward: connection.model("PromoReward", rewardSchema),
403
- Voucher: connection.model("PromoVoucher", voucherSchema),
404
- PendingEvaluation: connection.model("PromoPendingEvaluation", pendingEvaluationSchema)
420
+ Program: connection.model("PromoProgram", programSchema, prefix + DEFAULT_COLLECTIONS.PromoProgram),
421
+ Rule: connection.model("PromoRule", ruleSchema, prefix + DEFAULT_COLLECTIONS.PromoRule),
422
+ Reward: connection.model("PromoReward", rewardSchema, prefix + DEFAULT_COLLECTIONS.PromoReward),
423
+ Voucher: connection.model("PromoVoucher", voucherSchema, prefix + DEFAULT_COLLECTIONS.PromoVoucher),
424
+ PendingEvaluation: connection.model("PromoPendingEvaluation", pendingEvaluationSchema, prefix + DEFAULT_COLLECTIONS.PromoPendingEvaluation)
405
425
  };
406
426
  applyAutoIndex(result, autoIndex);
407
427
  return result;
@@ -490,8 +510,18 @@ var PendingEvaluationRepository = class extends Repository {
490
510
  //#endregion
491
511
  //#region src/events/dispatch.ts
492
512
  /**
513
+ * Context key for the post-commit publish queue. Promo's transactional
514
+ * verbs attach it automatically when they own the transaction; hosts that
515
+ * own the outer transaction (`ctx.session`) MAY attach their own array and
516
+ * drain it with {@link flushPendingPromoEvents} after commit for strict
517
+ * no-ghost-event semantics. Symbol-keyed so it survives `{ ...ctx }`
518
+ * spreads but never collides with host context fields.
519
+ */
520
+ const PENDING_PROMO_EVENTS = Symbol.for("@classytic/promo:pendingEvents");
521
+ /**
493
522
  * Canonical P8 shape — save to outbox first (with `ctx.session` when
494
- * present), then publish to transport. Both paths run independently.
523
+ * present), then publish to transport (deferred to the pending queue when
524
+ * one is attached — see module doc for the three regimes).
495
525
  */
496
526
  async function dispatchPromoEvent(deps, event, ctx) {
497
527
  const logger = deps.logger ?? console;
@@ -503,47 +533,57 @@ async function dispatchPromoEvent(deps, event, ctx) {
503
533
  logger.error(`[promo] outbox.save failed for ${event.type}:`, err);
504
534
  throw err;
505
535
  }
536
+ const pending = ctx?.[PENDING_PROMO_EVENTS];
537
+ if (pending) {
538
+ pending.push(event);
539
+ return;
540
+ }
506
541
  if (deps.events) try {
507
542
  await deps.events.publish(event);
508
543
  } catch (err) {
509
544
  logger.error(`[promo] events.publish failed for ${event.type}:`, err);
510
545
  }
511
546
  }
512
- //#endregion
513
- //#region src/events/event-constants.ts
514
- const PromoEvents = {
515
- PROGRAM_CREATED: "promo.program.created",
516
- PROGRAM_ACTIVATED: "promo.program.activated",
517
- PROGRAM_PAUSED: "promo.program.paused",
518
- PROGRAM_ARCHIVED: "promo.program.archived",
519
- RULE_ADDED: "promo.rule.added",
520
- RULE_UPDATED: "promo.rule.updated",
521
- RULE_REMOVED: "promo.rule.removed",
522
- REWARD_ADDED: "promo.reward.added",
523
- REWARD_UPDATED: "promo.reward.updated",
524
- REWARD_REMOVED: "promo.reward.removed",
525
- VOUCHER_GENERATED: "promo.voucher.generated",
526
- VOUCHER_REDEEMED: "promo.voucher.redeemed",
527
- VOUCHER_CANCELLED: "promo.voucher.cancelled",
528
- VOUCHER_EXPIRED: "promo.voucher.expired",
529
- GIFT_CARD_SPENT: "promo.gift_card.spent",
530
- GIFT_CARD_TOPPED_UP: "promo.gift_card.topped_up",
531
- GIFT_CARD_EXHAUSTED: "promo.gift_card.exhausted",
532
- EVALUATION_COMPLETED: "promo.evaluation.completed",
533
- EVALUATION_COMMITTED: "promo.evaluation.committed",
534
- EVALUATION_ROLLED_BACK: "promo.evaluation.rolled_back"
535
- };
547
+ /**
548
+ * Drain a pending-events queue through the transport, swallowing per-event
549
+ * publish errors (durable delivery is the outbox's job). Promo's
550
+ * transactional verbs call this after commit; hosts that attached their own
551
+ * `[PENDING_PROMO_EVENTS]` queue call it after their outer transaction
552
+ * commits. The queue is emptied in place.
553
+ */
554
+ async function flushPendingPromoEvents(deps, events) {
555
+ if (events.length === 0) return;
556
+ const logger = deps.logger ?? console;
557
+ const drained = events.splice(0, events.length);
558
+ if (!deps.events) return;
559
+ for (const event of drained) try {
560
+ await deps.events.publish(event);
561
+ } catch (err) {
562
+ logger.error(`[promo] events.publish failed for ${event.type}:`, err);
563
+ }
564
+ }
536
565
  //#endregion
537
566
  //#region src/events/helpers.ts
538
567
  function createEvent$1(type, payload, ctx, meta) {
539
568
  return createEvent(type, payload, {
540
569
  resource: "promo",
541
- ...ctx?.actorId !== void 0 ? { userId: ctx.actorId } : {},
570
+ ...ctx?.actorRef !== void 0 ? { userId: ctx.actorRef } : {},
542
571
  ...ctx?.organizationId !== void 0 ? { organizationId: ctx.organizationId } : {},
543
572
  ...meta
544
573
  });
545
574
  }
546
575
  //#endregion
576
+ //#region src/repositories/repo-options.ts
577
+ function repoOptionsFromCtx(ctx) {
578
+ const out = {};
579
+ if (!ctx) return out;
580
+ for (const key of Object.keys(ctx)) {
581
+ const value = ctx[key];
582
+ if (value !== void 0) out[key] = value;
583
+ }
584
+ return out;
585
+ }
586
+ //#endregion
547
587
  //#region src/repositories/program.repository.ts
548
588
  var ProgramRepository = class extends Repository {
549
589
  dispatchDeps;
@@ -564,14 +604,14 @@ var ProgramRepository = class extends Repository {
564
604
  const program = await this.getById(id, {
565
605
  throwOnNotFound: false,
566
606
  lean: true,
567
- ...ctx
607
+ ...repoOptionsFromCtx(ctx)
568
608
  });
569
609
  if (!program) throw new ProgramNotFoundError(id);
570
610
  PROGRAM_MACHINE.assertTransition(String(program._id), program.status, targetStatus);
571
611
  const updated = await this.update(id, { status: targetStatus }, {
572
612
  throwOnNotFound: true,
573
613
  lean: true,
574
- ...ctx
614
+ ...repoOptionsFromCtx(ctx)
575
615
  });
576
616
  if (!updated) throw new ProgramNotFoundError(id);
577
617
  const eventName = {
@@ -583,7 +623,7 @@ var ProgramRepository = class extends Repository {
583
623
  programId: updated._id,
584
624
  programType: updated.programType,
585
625
  status: updated.status,
586
- actorId: ctx.actorId
626
+ actorRef: ctx.actorRef
587
627
  }, ctx), ctx);
588
628
  return updated;
589
629
  }
@@ -732,13 +772,13 @@ var VoucherRepository = class extends Repository {
732
772
  const voucher = await this.getById(id, {
733
773
  throwOnNotFound: false,
734
774
  lean: true,
735
- ...ctx
775
+ ...repoOptionsFromCtx(ctx)
736
776
  });
737
777
  if (!voucher) throw new VoucherNotFoundError(id);
738
778
  const updated = await this.update(id, { status: "cancelled" }, {
739
779
  throwOnNotFound: true,
740
780
  lean: true,
741
- ...ctx
781
+ ...repoOptionsFromCtx(ctx)
742
782
  });
743
783
  if (!updated) throw new VoucherNotFoundError(id);
744
784
  await dispatchPromoEvent(this.dispatchDeps, createEvent$1(PromoEvents.VOUCHER_CANCELLED, {
@@ -798,11 +838,14 @@ var VoucherRepository = class extends Repository {
798
838
  ...ctx
799
839
  });
800
840
  let modified = 0;
801
- for (const doc of docs) if (await this.update(doc._id, { status: "expired" }, {
802
- throwOnNotFound: false,
803
- lean: true,
804
- ...ctx
805
- })) modified++;
841
+ for (const doc of docs) {
842
+ throwIfAborted(ctx?.signal);
843
+ if (await this.update(doc._id, { status: "expired" }, {
844
+ throwOnNotFound: false,
845
+ lean: true,
846
+ ...ctx
847
+ })) modified++;
848
+ }
806
849
  return modified;
807
850
  }
808
851
  /**
@@ -898,7 +941,7 @@ var MongoEvaluationStore = class {
898
941
  if (tenantField && tenantValue !== void 0) update[tenantField] = tenantValue;
899
942
  await this.repo.Model.updateOne(filter, { $set: update }, {
900
943
  upsert: true,
901
- session
944
+ ...session !== void 0 ? { session } : {}
902
945
  });
903
946
  }
904
947
  async take(id, ctx) {
@@ -1050,23 +1093,37 @@ var EvaluationService = class {
1050
1093
  }
1051
1094
  async commit(evaluationId, orderId, ctx, options = {}) {
1052
1095
  let snapshotForCapMapping = null;
1096
+ const run = async (txCtx) => {
1097
+ const session = txCtx.session;
1098
+ const stored = await this.store.take(evaluationId, {
1099
+ tenantValue: ctx.organizationId,
1100
+ session
1101
+ });
1102
+ if (!stored) throw new EvaluationNotFoundError(evaluationId);
1103
+ if (options.cartHash !== void 0 && options.cartHash !== stored.cartHash) throw new CartHashMismatchError(evaluationId);
1104
+ snapshotForCapMapping = stored;
1105
+ return await this.commitInTransaction(stored, evaluationId, orderId, txCtx);
1106
+ };
1053
1107
  try {
1054
- return await this.unitOfWork.withTransaction(async (session) => {
1055
- const stored = await this.store.take(evaluationId, {
1056
- tenantValue: ctx.organizationId,
1057
- session
1108
+ if (ctx.session) return await run(ctx);
1109
+ const pending = [];
1110
+ const result = await this.unitOfWork.withTransaction((session) => {
1111
+ pending.length = 0;
1112
+ return run({
1113
+ ...ctx,
1114
+ session,
1115
+ [PENDING_PROMO_EVENTS]: pending
1058
1116
  });
1059
- if (!stored) throw new EvaluationNotFoundError(evaluationId);
1060
- if (options.cartHash !== void 0 && options.cartHash !== stored.cartHash) throw new CartHashMismatchError(evaluationId);
1061
- snapshotForCapMapping = stored;
1062
- return await this.commitInTransaction(stored, evaluationId, orderId, session, ctx);
1063
1117
  });
1118
+ await flushPendingPromoEvents(this.dispatchDeps, pending);
1119
+ return result;
1064
1120
  } catch (err) {
1065
1121
  if (isWriteConflict(err) && snapshotForCapMapping) {
1066
1122
  const firstUsage = snapshotForCapMapping.programUsages[0];
1067
1123
  if (firstUsage) {
1124
+ const { session: _aborted, ...readCtx } = ctx;
1068
1125
  const program = await this.programRepo.getById(firstUsage.programId, {
1069
- ...ctx,
1126
+ ...repoOptionsFromCtx(readCtx),
1070
1127
  lean: true,
1071
1128
  throwOnNotFound: false
1072
1129
  });
@@ -1076,15 +1133,17 @@ var EvaluationService = class {
1076
1133
  throw err;
1077
1134
  }
1078
1135
  }
1079
- async commitInTransaction(stored, evaluationId, orderId, session, ctx) {
1136
+ async commitInTransaction(stored, evaluationId, orderId, txCtx) {
1137
+ const ctx = txCtx;
1138
+ const session = txCtx.session;
1080
1139
  for (const usage of stored.programUsages) {
1140
+ throwIfAborted(txCtx.signal);
1081
1141
  if (!await this.programRepo.tryIncrementUsage(usage.programId, {
1082
1142
  ...ctx,
1083
1143
  session
1084
1144
  })) {
1085
1145
  const program = await this.programRepo.getById(usage.programId, {
1086
- ...ctx,
1087
- session,
1146
+ ...repoOptionsFromCtx(ctx),
1088
1147
  lean: true,
1089
1148
  throwOnNotFound: false
1090
1149
  });
@@ -1115,14 +1174,14 @@ var EvaluationService = class {
1115
1174
  evaluationId,
1116
1175
  orderId,
1117
1176
  totalDiscount: stored.result.totalDiscount
1118
- }, ctx), {
1119
- ...ctx,
1120
- session
1121
- });
1177
+ }, txCtx), txCtx);
1122
1178
  return commitResult;
1123
1179
  }
1124
1180
  async rollback(evaluationId, ctx) {
1125
- await this.store.delete(evaluationId, { tenantValue: ctx.organizationId });
1181
+ await this.store.delete(evaluationId, {
1182
+ tenantValue: ctx.organizationId,
1183
+ session: ctx.session
1184
+ });
1126
1185
  await dispatchPromoEvent(this.dispatchDeps, createEvent$1(PromoEvents.EVALUATION_ROLLED_BACK, { evaluationId }, ctx), ctx);
1127
1186
  }
1128
1187
  async doEvaluate(input, ctx, isPreview) {
@@ -1145,7 +1204,7 @@ var EvaluationService = class {
1145
1204
  };
1146
1205
  const programs = await this.programRepo.findActive(void 0, ctx);
1147
1206
  const programIds = programs.map((p) => p._id);
1148
- const [allRules, allRewards] = await Promise.all([this.ruleRepo.findAll({ programId: { $in: programIds } }, ctx), this.rewardRepo.findAll({ programId: { $in: programIds } }, ctx)]);
1207
+ const [allRules, allRewards] = await Promise.all([this.ruleRepo.findAll({ programId: { $in: programIds } }, repoOptionsFromCtx(ctx)), this.rewardRepo.findAll({ programId: { $in: programIds } }, repoOptionsFromCtx(ctx))]);
1149
1208
  const rulesByProgram = groupBy(allRules, (r) => r.programId);
1150
1209
  const rewardsByProgram = groupBy(allRewards, (r) => r.programId);
1151
1210
  const voucherMap = /* @__PURE__ */ new Map();
@@ -1169,6 +1228,7 @@ var EvaluationService = class {
1169
1228
  let exclusiveApplied = false;
1170
1229
  let stackableCount = 0;
1171
1230
  for (const program of programs) {
1231
+ throwIfAborted(ctx.signal);
1172
1232
  if (program.maxUsageTotal && program.usedCount >= program.maxUsageTotal) continue;
1173
1233
  if (!this.isCustomerEligible(program, input)) continue;
1174
1234
  if (program.maxUsagePerCustomer && input.customerId) {
@@ -1252,16 +1312,20 @@ var EvaluationService = class {
1252
1312
  programsApplied
1253
1313
  };
1254
1314
  if (!isPreview) {
1315
+ const { session: _liveSession, ...persistableCtx } = ctx;
1255
1316
  const snapshot = {
1256
1317
  result,
1257
- ctx,
1318
+ ctx: persistableCtx,
1258
1319
  customerId: input.customerId,
1259
1320
  programUsages,
1260
1321
  voucherUsages,
1261
1322
  cartHash,
1262
1323
  createdAt: /* @__PURE__ */ new Date()
1263
1324
  };
1264
- await this.store.put(evaluationId, snapshot, DEFAULT_PENDING_EVALUATION_TTL_SECONDS, { tenantValue: ctx.organizationId });
1325
+ await this.store.put(evaluationId, snapshot, DEFAULT_PENDING_EVALUATION_TTL_SECONDS, {
1326
+ tenantValue: ctx.organizationId,
1327
+ session: ctx.session
1328
+ });
1265
1329
  }
1266
1330
  await dispatchPromoEvent(this.dispatchDeps, createEvent$1(PromoEvents.EVALUATION_COMPLETED, {
1267
1331
  evaluationId,
@@ -1424,11 +1488,46 @@ var VoucherService = class {
1424
1488
  this.dispatchDeps = dispatchDeps;
1425
1489
  this.config = config;
1426
1490
  }
1491
+ /**
1492
+ * Session threading — checkout-chain participation. When the host
1493
+ * already owns a transaction (e.g. the cart → order → flow → promo →
1494
+ * invoice chain runs as ONE atomic unit), it passes `ctx.session` and
1495
+ * the transactional body JOINS that session: every write commits or
1496
+ * aborts with the host's order. MongoDB has no nested transactions, so
1497
+ * we must NOT open our own in that case — the host owns commit/abort/
1498
+ * retry. Without a host session we own the transaction via the
1499
+ * UnitOfWork (mongokit retry semantics included).
1500
+ *
1501
+ * **Post-commit publish (§P8 phased variant).** When promo owns the
1502
+ * transaction, a pending queue is attached to the tx context so every
1503
+ * `dispatchPromoEvent` inside the body defers its transport publish; the
1504
+ * queue flushes only after the transaction commits — a rollback (or a
1505
+ * transient retry of the body) can't leak ghost events to in-process
1506
+ * subscribers. The queue resets at the start of each retry attempt.
1507
+ * When the HOST owns the session, promo can't know when the host
1508
+ * commits: with a host-attached `[PENDING_PROMO_EVENTS]` queue the
1509
+ * deferral still applies (host flushes); without one, publish happens
1510
+ * in-scope as the documented best effort (see events/dispatch.ts).
1511
+ */
1512
+ async runTransactional(ctx, fn) {
1513
+ if (ctx.session) return fn(ctx);
1514
+ const pending = [];
1515
+ const result = await this.unitOfWork.withTransaction((session) => {
1516
+ pending.length = 0;
1517
+ return fn({
1518
+ ...ctx,
1519
+ session,
1520
+ [PENDING_PROMO_EVENTS]: pending
1521
+ });
1522
+ });
1523
+ await flushPendingPromoEvents(this.dispatchDeps, pending);
1524
+ return result;
1525
+ }
1427
1526
  async generateCodes(input, ctx) {
1428
1527
  const program = await this.programRepo.getById(input.programId, {
1429
1528
  throwOnNotFound: false,
1430
1529
  lean: true,
1431
- ...ctx
1530
+ ...repoOptionsFromCtx(ctx)
1432
1531
  });
1433
1532
  if (!program) throw new ProgramNotFoundError(input.programId);
1434
1533
  const { codeLength, codePrefix } = this.config.voucher;
@@ -1451,13 +1550,19 @@ var VoucherService = class {
1451
1550
  redemptions: [],
1452
1551
  ...input.metadata ? { metadata: input.metadata } : {}
1453
1552
  }));
1454
- const vouchers = await this.voucherRepo.createMany(data, ctx);
1553
+ let vouchers;
1554
+ try {
1555
+ vouchers = await this.voucherRepo.createMany(data, repoOptionsFromCtx(ctx));
1556
+ } catch (err) {
1557
+ if (isDuplicateKeyError(err)) throw new DuplicateVoucherCodeError();
1558
+ throw err;
1559
+ }
1455
1560
  await dispatchPromoEvent(this.dispatchDeps, createEvent$1(PromoEvents.VOUCHER_GENERATED, {
1456
1561
  programId: input.programId,
1457
1562
  voucherIds: vouchers.map((v) => v._id),
1458
1563
  codes: vouchers.map((v) => v.code),
1459
1564
  count: vouchers.length,
1460
- actorId: ctx.actorId
1565
+ actorRef: ctx.actorRef
1461
1566
  }, ctx), ctx);
1462
1567
  return vouchers;
1463
1568
  }
@@ -1465,7 +1570,7 @@ var VoucherService = class {
1465
1570
  const program = await this.programRepo.getById(input.programId, {
1466
1571
  throwOnNotFound: false,
1467
1572
  lean: true,
1468
- ...ctx
1573
+ ...repoOptionsFromCtx(ctx)
1469
1574
  });
1470
1575
  if (!program) throw new ProgramNotFoundError(input.programId);
1471
1576
  const code = input.code?.toUpperCase() ?? generateCode(this.config.voucher.codeLength, this.config.voucher.codePrefix);
@@ -1488,13 +1593,19 @@ var VoucherService = class {
1488
1593
  redemptions: [],
1489
1594
  ...input.metadata ? { metadata: input.metadata } : {}
1490
1595
  };
1491
- const voucher = await this.voucherRepo.create(data, ctx);
1596
+ let voucher;
1597
+ try {
1598
+ voucher = await this.voucherRepo.create(data, repoOptionsFromCtx(ctx));
1599
+ } catch (err) {
1600
+ if (isDuplicateKeyError(err)) throw new DuplicateVoucherCodeError(code);
1601
+ throw err;
1602
+ }
1492
1603
  await dispatchPromoEvent(this.dispatchDeps, createEvent$1(PromoEvents.VOUCHER_GENERATED, {
1493
1604
  programId: input.programId,
1494
1605
  voucherIds: [voucher._id],
1495
1606
  codes: [voucher.code],
1496
1607
  count: 1,
1497
- actorId: ctx.actorId
1608
+ actorRef: ctx.actorRef
1498
1609
  }, ctx), ctx);
1499
1610
  return voucher;
1500
1611
  }
@@ -1527,7 +1638,7 @@ var VoucherService = class {
1527
1638
  const program = await this.programRepo.getById(voucher.programId, {
1528
1639
  throwOnNotFound: false,
1529
1640
  lean: true,
1530
- ...ctx
1641
+ ...repoOptionsFromCtx(ctx)
1531
1642
  });
1532
1643
  return {
1533
1644
  valid: true,
@@ -1543,18 +1654,12 @@ var VoucherService = class {
1543
1654
  };
1544
1655
  }
1545
1656
  async redeem(input, ctx) {
1546
- return this.unitOfWork.withTransaction(async (session) => {
1547
- const voucher = await this.voucherRepo.getByCode(input.code, {
1548
- ...ctx,
1549
- session
1550
- });
1657
+ return this.runTransactional(ctx, async (txCtx) => {
1658
+ const voucher = await this.voucherRepo.getByCode(input.code, txCtx);
1551
1659
  if (!voucher) throw new VoucherNotFoundError(input.code);
1552
1660
  this.assertVoucherUsable(voucher);
1553
1661
  if (input.idempotencyKey) {
1554
- if (await this.voucherRepo.hasIdempotencyKey(voucher._id, input.idempotencyKey, {
1555
- ...ctx,
1556
- session
1557
- })) throw new DuplicateRedemptionError(input.idempotencyKey);
1662
+ if (await this.voucherRepo.hasIdempotencyKey(voucher._id, input.idempotencyKey, txCtx)) throw new DuplicateRedemptionError(input.idempotencyKey);
1558
1663
  }
1559
1664
  const redemption = {
1560
1665
  orderId: input.orderId,
@@ -1564,14 +1669,10 @@ var VoucherService = class {
1564
1669
  ...input.idempotencyKey ? { idempotencyKey: input.idempotencyKey } : {},
1565
1670
  ...ctx.organizationId !== void 0 ? { organizationId: ctx.organizationId } : {}
1566
1671
  };
1567
- const updated = await this.voucherRepo.incrementUsage(voucher._id, redemption, {
1568
- ...ctx,
1569
- session
1570
- });
1672
+ const updated = await this.voucherRepo.incrementUsage(voucher._id, redemption, txCtx);
1571
1673
  if (updated.usedCount >= updated.usageLimit) await this.voucherRepo.update(voucher._id, { status: "used" }, {
1572
1674
  lean: true,
1573
- ...ctx,
1574
- session
1675
+ ...repoOptionsFromCtx(txCtx)
1575
1676
  });
1576
1677
  await dispatchPromoEvent(this.dispatchDeps, createEvent$1(PromoEvents.VOUCHER_REDEEMED, {
1577
1678
  voucherId: voucher._id,
@@ -1579,10 +1680,7 @@ var VoucherService = class {
1579
1680
  orderId: input.orderId,
1580
1681
  discountAmount: input.discountAmount,
1581
1682
  customerId: input.customerId
1582
- }, ctx), {
1583
- ...ctx,
1584
- session
1585
- });
1683
+ }, txCtx), txCtx);
1586
1684
  return updated;
1587
1685
  });
1588
1686
  }
@@ -1608,20 +1706,14 @@ var VoucherService = class {
1608
1706
  }
1609
1707
  }
1610
1708
  async spendInTransaction(input, ctx) {
1611
- return this.unitOfWork.withTransaction(async (session) => {
1612
- const voucher = await this.voucherRepo.getByCode(input.code, {
1613
- ...ctx,
1614
- session
1615
- });
1709
+ return this.runTransactional(ctx, async (txCtx) => {
1710
+ const voucher = await this.voucherRepo.getByCode(input.code, txCtx);
1616
1711
  if (!voucher) throw new VoucherNotFoundError(input.code);
1617
1712
  this.assertVoucherUsable(voucher);
1618
1713
  const balance = voucher.currentBalance ?? 0;
1619
1714
  if (!this.config.giftCard.allowNegativeBalance && balance < input.amount) throw new InsufficientBalanceError(input.code, balance, input.amount);
1620
1715
  if (input.idempotencyKey) {
1621
- if (await this.voucherRepo.hasIdempotencyKey(voucher._id, input.idempotencyKey, {
1622
- ...ctx,
1623
- session
1624
- })) throw new DuplicateRedemptionError(input.idempotencyKey);
1716
+ if (await this.voucherRepo.hasIdempotencyKey(voucher._id, input.idempotencyKey, txCtx)) throw new DuplicateRedemptionError(input.idempotencyKey);
1625
1717
  }
1626
1718
  const entry = {
1627
1719
  amount: -input.amount,
@@ -1631,10 +1723,7 @@ var VoucherService = class {
1631
1723
  ...input.idempotencyKey ? { idempotencyKey: input.idempotencyKey } : {},
1632
1724
  ...ctx.organizationId !== void 0 ? { organizationId: ctx.organizationId } : {}
1633
1725
  };
1634
- const updated = await this.voucherRepo.addLedgerEntry(voucher._id, entry, -input.amount, {
1635
- ...ctx,
1636
- session
1637
- });
1726
+ const updated = await this.voucherRepo.addLedgerEntry(voucher._id, entry, -input.amount, txCtx);
1638
1727
  const newBalance = updated.currentBalance ?? 0;
1639
1728
  await dispatchPromoEvent(this.dispatchDeps, createEvent$1(PromoEvents.GIFT_CARD_SPENT, {
1640
1729
  voucherId: voucher._id,
@@ -1642,24 +1731,17 @@ var VoucherService = class {
1642
1731
  amount: input.amount,
1643
1732
  remainingBalance: newBalance,
1644
1733
  orderId: input.orderId
1645
- }, ctx), {
1646
- ...ctx,
1647
- session
1648
- });
1734
+ }, txCtx), txCtx);
1649
1735
  if (newBalance <= 0) {
1650
1736
  await this.voucherRepo.update(voucher._id, { status: "used" }, {
1651
1737
  lean: true,
1652
- ...ctx,
1653
- session
1738
+ ...repoOptionsFromCtx(txCtx)
1654
1739
  });
1655
1740
  await dispatchPromoEvent(this.dispatchDeps, createEvent$1(PromoEvents.GIFT_CARD_EXHAUSTED, {
1656
1741
  voucherId: voucher._id,
1657
1742
  code: voucher.code,
1658
1743
  status: "used"
1659
- }, ctx), {
1660
- ...ctx,
1661
- session
1662
- });
1744
+ }, txCtx), txCtx);
1663
1745
  }
1664
1746
  return {
1665
1747
  code: updated.code,
@@ -1680,19 +1762,13 @@ var VoucherService = class {
1680
1762
  }
1681
1763
  }
1682
1764
  async topUpInTransaction(input, ctx) {
1683
- return this.unitOfWork.withTransaction(async (session) => {
1684
- const voucher = await this.voucherRepo.getByCode(input.code, {
1685
- ...ctx,
1686
- session
1687
- });
1765
+ return this.runTransactional(ctx, async (txCtx) => {
1766
+ const voucher = await this.voucherRepo.getByCode(input.code, txCtx);
1688
1767
  if (!voucher) throw new VoucherNotFoundError(input.code);
1689
1768
  const maxBalance = this.config.giftCard.maxBalance;
1690
1769
  if (maxBalance !== null && (voucher.currentBalance ?? 0) + input.amount > maxBalance) throw new ValidationError(`Top-up would exceed max balance of ${maxBalance}`);
1691
1770
  if (input.idempotencyKey) {
1692
- if (await this.voucherRepo.hasIdempotencyKey(voucher._id, input.idempotencyKey, {
1693
- ...ctx,
1694
- session
1695
- })) throw new DuplicateRedemptionError(input.idempotencyKey);
1771
+ if (await this.voucherRepo.hasIdempotencyKey(voucher._id, input.idempotencyKey, txCtx)) throw new DuplicateRedemptionError(input.idempotencyKey);
1696
1772
  }
1697
1773
  const entry = {
1698
1774
  amount: input.amount,
@@ -1701,24 +1777,17 @@ var VoucherService = class {
1701
1777
  ...input.idempotencyKey ? { idempotencyKey: input.idempotencyKey } : {},
1702
1778
  ...ctx.organizationId !== void 0 ? { organizationId: ctx.organizationId } : {}
1703
1779
  };
1704
- const updated = await this.voucherRepo.addLedgerEntry(voucher._id, entry, input.amount, {
1705
- ...ctx,
1706
- session
1707
- });
1780
+ const updated = await this.voucherRepo.addLedgerEntry(voucher._id, entry, input.amount, txCtx);
1708
1781
  if (voucher.status === "used") await this.voucherRepo.update(voucher._id, { status: "active" }, {
1709
1782
  lean: true,
1710
- ...ctx,
1711
- session
1783
+ ...repoOptionsFromCtx(txCtx)
1712
1784
  });
1713
1785
  await dispatchPromoEvent(this.dispatchDeps, createEvent$1(PromoEvents.GIFT_CARD_TOPPED_UP, {
1714
1786
  voucherId: voucher._id,
1715
1787
  code: voucher.code,
1716
1788
  amount: input.amount,
1717
1789
  newBalance: updated.currentBalance ?? 0
1718
- }, ctx), {
1719
- ...ctx,
1720
- session
1721
- });
1790
+ }, txCtx), txCtx);
1722
1791
  return {
1723
1792
  code: updated.code,
1724
1793
  initialBalance: updated.initialBalance ?? 0,
@@ -1751,229 +1820,6 @@ function createServices(deps) {
1751
1820
  };
1752
1821
  }
1753
1822
  //#endregion
1754
- //#region src/events/promo-event-catalog.ts
1755
- function definePromoEvent(input) {
1756
- const { name, version = 1, description, zodSchema } = input;
1757
- const def = {
1758
- name,
1759
- version,
1760
- schema: z.toJSONSchema(zodSchema),
1761
- zodSchema,
1762
- create(payload, meta) {
1763
- return createEvent(name, payload, {
1764
- source: "promo",
1765
- ...meta
1766
- });
1767
- }
1768
- };
1769
- if (description !== void 0) def.description = description;
1770
- return def;
1771
- }
1772
- /** Mirrors `ProgramLifecyclePayload`. */
1773
- const programLifecycleSchema = z.object({
1774
- programId: z.string(),
1775
- programType: z.string(),
1776
- status: z.string(),
1777
- actorId: z.string().optional()
1778
- });
1779
- /** Mirrors `RulePayload`. */
1780
- const ruleSchema = z.object({
1781
- programId: z.string(),
1782
- ruleId: z.string(),
1783
- actorId: z.string().optional()
1784
- });
1785
- /** Mirrors `RewardPayload`. */
1786
- const rewardSchema = z.object({
1787
- programId: z.string(),
1788
- rewardId: z.string(),
1789
- actorId: z.string().optional()
1790
- });
1791
- /** Mirrors `VoucherGeneratedPayload`. */
1792
- const voucherGeneratedSchema = z.object({
1793
- programId: z.string(),
1794
- voucherIds: z.array(z.string()),
1795
- codes: z.array(z.string()),
1796
- count: z.number().int().nonnegative(),
1797
- actorId: z.string().optional()
1798
- });
1799
- /** Mirrors `VoucherRedeemedPayload`. */
1800
- const voucherRedeemedSchema = z.object({
1801
- voucherId: z.string(),
1802
- code: z.string(),
1803
- orderId: z.string(),
1804
- discountAmount: z.number(),
1805
- customerId: z.string().optional()
1806
- });
1807
- /**
1808
- * Mirrors `VoucherLifecyclePayload` — shared by VOUCHER_CANCELLED,
1809
- * VOUCHER_EXPIRED, and GIFT_CARD_EXHAUSTED (repo emits `status: 'cancelled'`
1810
- * / `'used'` / host-supplied terminal value).
1811
- */
1812
- const voucherLifecycleSchema = z.object({
1813
- voucherId: z.string(),
1814
- code: z.string(),
1815
- status: z.string()
1816
- });
1817
- /** Mirrors `GiftCardSpentPayload`. */
1818
- const giftCardSpentSchema = z.object({
1819
- voucherId: z.string(),
1820
- code: z.string(),
1821
- amount: z.number(),
1822
- remainingBalance: z.number(),
1823
- orderId: z.string()
1824
- });
1825
- /** Mirrors `GiftCardToppedUpPayload`. */
1826
- const giftCardToppedUpSchema = z.object({
1827
- voucherId: z.string(),
1828
- code: z.string(),
1829
- amount: z.number(),
1830
- newBalance: z.number()
1831
- });
1832
- /** Mirrors `EvaluationCompletedPayload`. */
1833
- const evaluationCompletedSchema = z.object({
1834
- evaluationId: z.string(),
1835
- totalDiscount: z.number(),
1836
- programsApplied: z.number().int().nonnegative(),
1837
- codesUsed: z.array(z.string()),
1838
- isPreview: z.boolean()
1839
- });
1840
- /** Mirrors `EvaluationCommittedPayload`. */
1841
- const evaluationCommittedSchema = z.object({
1842
- evaluationId: z.string(),
1843
- orderId: z.string(),
1844
- totalDiscount: z.number()
1845
- });
1846
- /** Single-field rollback payload — emitted by `evaluation.service.ts`. */
1847
- const evaluationRolledBackSchema = z.object({ evaluationId: z.string() });
1848
- const ProgramCreated = definePromoEvent({
1849
- name: PromoEvents.PROGRAM_CREATED,
1850
- description: "A new promo program was created (starts in draft).",
1851
- zodSchema: programLifecycleSchema
1852
- });
1853
- const ProgramActivated = definePromoEvent({
1854
- name: PromoEvents.PROGRAM_ACTIVATED,
1855
- description: "A draft or paused program was activated.",
1856
- zodSchema: programLifecycleSchema
1857
- });
1858
- const ProgramPaused = definePromoEvent({
1859
- name: PromoEvents.PROGRAM_PAUSED,
1860
- description: "An active program was paused.",
1861
- zodSchema: programLifecycleSchema
1862
- });
1863
- const ProgramArchived = definePromoEvent({
1864
- name: PromoEvents.PROGRAM_ARCHIVED,
1865
- description: "A program was archived (terminal — no further transitions).",
1866
- zodSchema: programLifecycleSchema
1867
- });
1868
- const RuleAdded = definePromoEvent({
1869
- name: PromoEvents.RULE_ADDED,
1870
- description: "A new rule was added to a program.",
1871
- zodSchema: ruleSchema
1872
- });
1873
- const RuleUpdated = definePromoEvent({
1874
- name: PromoEvents.RULE_UPDATED,
1875
- description: "An existing rule on a program was updated.",
1876
- zodSchema: ruleSchema
1877
- });
1878
- const RuleRemoved = definePromoEvent({
1879
- name: PromoEvents.RULE_REMOVED,
1880
- description: "A rule was removed from a program.",
1881
- zodSchema: ruleSchema
1882
- });
1883
- const RewardAdded = definePromoEvent({
1884
- name: PromoEvents.REWARD_ADDED,
1885
- description: "A reward was added to a program.",
1886
- zodSchema: rewardSchema
1887
- });
1888
- const RewardUpdated = definePromoEvent({
1889
- name: PromoEvents.REWARD_UPDATED,
1890
- description: "A reward on a program was updated.",
1891
- zodSchema: rewardSchema
1892
- });
1893
- const RewardRemoved = definePromoEvent({
1894
- name: PromoEvents.REWARD_REMOVED,
1895
- description: "A reward was removed from a program.",
1896
- zodSchema: rewardSchema
1897
- });
1898
- const VoucherGenerated = definePromoEvent({
1899
- name: PromoEvents.VOUCHER_GENERATED,
1900
- description: "One or more vouchers were generated for a program.",
1901
- zodSchema: voucherGeneratedSchema
1902
- });
1903
- const VoucherRedeemed = definePromoEvent({
1904
- name: PromoEvents.VOUCHER_REDEEMED,
1905
- description: "A voucher was redeemed against an order.",
1906
- zodSchema: voucherRedeemedSchema
1907
- });
1908
- const VoucherCancelled = definePromoEvent({
1909
- name: PromoEvents.VOUCHER_CANCELLED,
1910
- description: "A voucher was cancelled by an operator.",
1911
- zodSchema: voucherLifecycleSchema
1912
- });
1913
- const VoucherExpired = definePromoEvent({
1914
- name: PromoEvents.VOUCHER_EXPIRED,
1915
- description: "A voucher reached its expiry date and was transitioned to expired.",
1916
- zodSchema: voucherLifecycleSchema
1917
- });
1918
- const GiftCardSpent = definePromoEvent({
1919
- name: PromoEvents.GIFT_CARD_SPENT,
1920
- description: "A gift-card voucher was debited against an order — remaining balance reported.",
1921
- zodSchema: giftCardSpentSchema
1922
- });
1923
- const GiftCardToppedUp = definePromoEvent({
1924
- name: PromoEvents.GIFT_CARD_TOPPED_UP,
1925
- description: "A gift-card voucher received a top-up — new balance reported.",
1926
- zodSchema: giftCardToppedUpSchema
1927
- });
1928
- const GiftCardExhausted = definePromoEvent({
1929
- name: PromoEvents.GIFT_CARD_EXHAUSTED,
1930
- description: "A gift-card voucher reached a zero balance and was marked used.",
1931
- zodSchema: voucherLifecycleSchema
1932
- });
1933
- const EvaluationCompleted = definePromoEvent({
1934
- name: PromoEvents.EVALUATION_COMPLETED,
1935
- description: "A cart evaluation finished (preview or pre-commit) — totals and applied codes reported.",
1936
- zodSchema: evaluationCompletedSchema
1937
- });
1938
- const EvaluationCommitted = definePromoEvent({
1939
- name: PromoEvents.EVALUATION_COMMITTED,
1940
- description: "A stored evaluation was committed against an order — usage counters moved.",
1941
- zodSchema: evaluationCommittedSchema
1942
- });
1943
- const EvaluationRolledBack = definePromoEvent({
1944
- name: PromoEvents.EVALUATION_ROLLED_BACK,
1945
- description: "A stored evaluation was discarded without commit.",
1946
- zodSchema: evaluationRolledBackSchema
1947
- });
1948
- /**
1949
- * Every promo event defined in the package — pass to Arc's
1950
- * `EventRegistry`. Hosts wire ONE array; the whole `promo.*` namespace
1951
- * becomes introspectable via OpenAPI and auto-validated at publish time
1952
- * when `eventPlugin({ validateMode: 'reject' })` is set.
1953
- */
1954
- const promoEventDefinitions = [
1955
- ProgramCreated,
1956
- ProgramActivated,
1957
- ProgramPaused,
1958
- ProgramArchived,
1959
- RuleAdded,
1960
- RuleUpdated,
1961
- RuleRemoved,
1962
- RewardAdded,
1963
- RewardUpdated,
1964
- RewardRemoved,
1965
- VoucherGenerated,
1966
- VoucherRedeemed,
1967
- VoucherCancelled,
1968
- VoucherExpired,
1969
- GiftCardSpent,
1970
- GiftCardToppedUp,
1971
- GiftCardExhausted,
1972
- EvaluationCompleted,
1973
- EvaluationCommitted,
1974
- EvaluationRolledBack
1975
- ];
1976
- //#endregion
1977
1823
  //#region src/index.ts
1978
1824
  function resolveConfig(config) {
1979
1825
  const tenant = resolveTenantConfig(config.tenant);
@@ -2013,14 +1859,30 @@ function resolveConfig(config) {
2013
1859
  * that wires it. See packages/mongokit/src/transaction.ts.
2014
1860
  */
2015
1861
  var MongoUnitOfWork = class {
2016
- constructor(connection) {
1862
+ constructor(connection, allowFallback = false) {
2017
1863
  this.connection = connection;
1864
+ this.allowFallback = allowFallback;
2018
1865
  }
2019
1866
  withTransaction(cb) {
2020
- return withTransaction(this.connection, cb);
1867
+ return withTransaction(this.connection, cb, this.allowFallback ? { allowFallback: true } : {});
2021
1868
  }
2022
1869
  };
2023
1870
  /**
1871
+ * Boot-time capability gate (PACKAGE_RULES "Runtime capabilities").
1872
+ * commit / redeem / spend / topUp are transactional — assert the backend
1873
+ * declares `transactions` at engine creation so misconfigured deployments
1874
+ * fail loud at boot instead of with a cryptic error on the first checkout.
1875
+ * `allowNonTransactional: true` is the explicit opt-out for standalone
1876
+ * MongoDB (dev / CI) — the unit-of-work then falls back to
1877
+ * non-transactional execution.
1878
+ *
1879
+ * Exported for unit testing; hosts never call it directly.
1880
+ */
1881
+ function assertPromoCapabilities(capabilities) {
1882
+ if (!capabilities) throw new Error("promo: backend declares no `capabilities` descriptor (repo-core 0.6 / mongokit 3.16+). Upgrade @classytic/mongokit, or pass `allowNonTransactional: true` to opt out.");
1883
+ if (capabilities.transactions !== true) throw new Error("promo: backend missing required capability: transactions. commit/redeem/spend/topUp are transactional — MongoDB must run as a replica set. Pass `allowNonTransactional: true` to accept best-effort atomicity (dev/CI only).");
1884
+ }
1885
+ /**
2024
1886
  * Build the promo engine for a host application.
2025
1887
  *
2026
1888
  * **Index management — important for boot performance:**
@@ -2059,7 +1921,12 @@ var MongoUnitOfWork = class {
2059
1921
  */
2060
1922
  function createPromoEngine(config) {
2061
1923
  const resolvedConfig = resolveConfig(config);
2062
- const models = createModels(config.mongoose, resolvedConfig.tenant, config.indexes, config.autoIndex);
1924
+ const models = createModels(config.mongoose, resolvedConfig.tenant, {
1925
+ indexes: config.indexes,
1926
+ autoIndex: config.autoIndex,
1927
+ collectionPrefix: config.collectionPrefix,
1928
+ forceRecreate: config.forceRecreate
1929
+ });
2063
1930
  const events = config.events?.transport ?? new InProcessPromoBus();
2064
1931
  const dispatchDeps = {
2065
1932
  events,
@@ -2067,12 +1934,14 @@ function createPromoEngine(config) {
2067
1934
  ...config.logger !== void 0 ? { logger: config.logger } : {}
2068
1935
  };
2069
1936
  const repositories = createRepositories(models, config.plugins, resolvedConfig.tenant, dispatchDeps);
1937
+ const allowNonTransactional = config.allowNonTransactional ?? false;
1938
+ if (!allowNonTransactional) assertPromoCapabilities(repositories.program.capabilities);
2070
1939
  return {
2071
1940
  models,
2072
1941
  repositories,
2073
1942
  services: createServices({
2074
1943
  repositories,
2075
- unitOfWork: new MongoUnitOfWork(config.mongoose),
1944
+ unitOfWork: new MongoUnitOfWork(config.mongoose, allowNonTransactional),
2076
1945
  dispatchDeps,
2077
1946
  config: resolvedConfig,
2078
1947
  ...config.evaluationStore !== void 0 ? { evaluationStore: config.evaluationStore } : {}
@@ -2084,4 +1953,4 @@ function createPromoEngine(config) {
2084
1953
  };
2085
1954
  }
2086
1955
  //#endregion
2087
- export { CartHashMismatchError, ConcurrencyConflictError, DuplicateRedemptionError, EvaluationCommitted, EvaluationCompleted, EvaluationNotFoundError, EvaluationRolledBack, GiftCardExhausted, GiftCardExhaustedError, GiftCardSpent, GiftCardToppedUp, InMemoryEvaluationStore, InsufficientBalanceError, InvalidTransitionError, MongoEvaluationStore, PendingEvaluationRepository, ProgramActivated, ProgramArchived, ProgramCreated, ProgramNotFoundError, ProgramPaused, ProgramRepository, ProgramUsageCapExceededError, PromoError, PromoEvents, RewardAdded, RewardNotFoundError, RewardRemoved, RewardRepository, RewardUpdated, RuleAdded, RuleNotFoundError, RuleRemoved, RuleRepository, RuleUpdated, TenantIsolationError, ValidationError, VoucherCancelled, VoucherExhaustedError, VoucherExpired, VoucherExpiredError, VoucherGenerated, VoucherNotFoundError, VoucherRedeemed, VoucherRepository, createPromoEngine, promoEventDefinitions, resolveConfig };
1956
+ export { CartHashMismatchError, ConcurrencyConflictError, DEFAULT_COLLECTIONS, DuplicateRedemptionError, DuplicateVoucherCodeError, EvaluationCommitted, EvaluationCompleted, EvaluationNotFoundError, EvaluationRolledBack, GiftCardExhausted, GiftCardExhaustedError, GiftCardSpent, GiftCardToppedUp, InMemoryEvaluationStore, InsufficientBalanceError, InvalidTransitionError, MongoEvaluationStore, PENDING_PROMO_EVENTS, PendingEvaluationRepository, ProgramActivated, ProgramArchived, ProgramCreated, ProgramNotFoundError, ProgramPaused, ProgramRepository, ProgramUsageCapExceededError, PromoError, PromoEvents, PromoModelCollisionError, RewardAdded, RewardNotFoundError, RewardRemoved, RewardRepository, RewardUpdated, RuleAdded, RuleNotFoundError, RuleRemoved, RuleRepository, RuleUpdated, TenantIsolationError, ValidationError, VoucherCancelled, VoucherExhaustedError, VoucherExpired, VoucherExpiredError, VoucherGenerated, VoucherNotFoundError, VoucherRedeemed, VoucherRepository, assertPromoCapabilities, createPromoEngine, flushPendingPromoEvents, promoEventDefinitions, resolveConfig };