@classytic/promo 0.2.3 → 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;
@@ -501,6 +531,12 @@ async function dispatchPromoEvent(deps, event, ctx) {
501
531
  await deps.outbox.save(event, saveOptions);
502
532
  } catch (err) {
503
533
  logger.error(`[promo] outbox.save failed for ${event.type}:`, err);
534
+ throw err;
535
+ }
536
+ const pending = ctx?.[PENDING_PROMO_EVENTS];
537
+ if (pending) {
538
+ pending.push(event);
539
+ return;
504
540
  }
505
541
  if (deps.events) try {
506
542
  await deps.events.publish(event);
@@ -508,41 +544,46 @@ async function dispatchPromoEvent(deps, event, ctx) {
508
544
  logger.error(`[promo] events.publish failed for ${event.type}:`, err);
509
545
  }
510
546
  }
511
- //#endregion
512
- //#region src/events/event-constants.ts
513
- const PromoEvents = {
514
- PROGRAM_CREATED: "promo.program.created",
515
- PROGRAM_ACTIVATED: "promo.program.activated",
516
- PROGRAM_PAUSED: "promo.program.paused",
517
- PROGRAM_ARCHIVED: "promo.program.archived",
518
- RULE_ADDED: "promo.rule.added",
519
- RULE_UPDATED: "promo.rule.updated",
520
- RULE_REMOVED: "promo.rule.removed",
521
- REWARD_ADDED: "promo.reward.added",
522
- REWARD_UPDATED: "promo.reward.updated",
523
- REWARD_REMOVED: "promo.reward.removed",
524
- VOUCHER_GENERATED: "promo.voucher.generated",
525
- VOUCHER_REDEEMED: "promo.voucher.redeemed",
526
- VOUCHER_CANCELLED: "promo.voucher.cancelled",
527
- VOUCHER_EXPIRED: "promo.voucher.expired",
528
- GIFT_CARD_SPENT: "promo.gift_card.spent",
529
- GIFT_CARD_TOPPED_UP: "promo.gift_card.topped_up",
530
- GIFT_CARD_EXHAUSTED: "promo.gift_card.exhausted",
531
- EVALUATION_COMPLETED: "promo.evaluation.completed",
532
- EVALUATION_COMMITTED: "promo.evaluation.committed",
533
- EVALUATION_ROLLED_BACK: "promo.evaluation.rolled_back"
534
- };
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
+ }
535
565
  //#endregion
536
566
  //#region src/events/helpers.ts
537
567
  function createEvent$1(type, payload, ctx, meta) {
538
568
  return createEvent(type, payload, {
539
569
  resource: "promo",
540
- ...ctx?.actorId !== void 0 ? { userId: ctx.actorId } : {},
570
+ ...ctx?.actorRef !== void 0 ? { userId: ctx.actorRef } : {},
541
571
  ...ctx?.organizationId !== void 0 ? { organizationId: ctx.organizationId } : {},
542
572
  ...meta
543
573
  });
544
574
  }
545
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
546
587
  //#region src/repositories/program.repository.ts
547
588
  var ProgramRepository = class extends Repository {
548
589
  dispatchDeps;
@@ -563,14 +604,14 @@ var ProgramRepository = class extends Repository {
563
604
  const program = await this.getById(id, {
564
605
  throwOnNotFound: false,
565
606
  lean: true,
566
- ...ctx
607
+ ...repoOptionsFromCtx(ctx)
567
608
  });
568
609
  if (!program) throw new ProgramNotFoundError(id);
569
610
  PROGRAM_MACHINE.assertTransition(String(program._id), program.status, targetStatus);
570
611
  const updated = await this.update(id, { status: targetStatus }, {
571
612
  throwOnNotFound: true,
572
613
  lean: true,
573
- ...ctx
614
+ ...repoOptionsFromCtx(ctx)
574
615
  });
575
616
  if (!updated) throw new ProgramNotFoundError(id);
576
617
  const eventName = {
@@ -582,7 +623,7 @@ var ProgramRepository = class extends Repository {
582
623
  programId: updated._id,
583
624
  programType: updated.programType,
584
625
  status: updated.status,
585
- actorId: ctx.actorId
626
+ actorRef: ctx.actorRef
586
627
  }, ctx), ctx);
587
628
  return updated;
588
629
  }
@@ -731,13 +772,13 @@ var VoucherRepository = class extends Repository {
731
772
  const voucher = await this.getById(id, {
732
773
  throwOnNotFound: false,
733
774
  lean: true,
734
- ...ctx
775
+ ...repoOptionsFromCtx(ctx)
735
776
  });
736
777
  if (!voucher) throw new VoucherNotFoundError(id);
737
778
  const updated = await this.update(id, { status: "cancelled" }, {
738
779
  throwOnNotFound: true,
739
780
  lean: true,
740
- ...ctx
781
+ ...repoOptionsFromCtx(ctx)
741
782
  });
742
783
  if (!updated) throw new VoucherNotFoundError(id);
743
784
  await dispatchPromoEvent(this.dispatchDeps, createEvent$1(PromoEvents.VOUCHER_CANCELLED, {
@@ -797,11 +838,14 @@ var VoucherRepository = class extends Repository {
797
838
  ...ctx
798
839
  });
799
840
  let modified = 0;
800
- for (const doc of docs) if (await this.update(doc._id, { status: "expired" }, {
801
- throwOnNotFound: false,
802
- lean: true,
803
- ...ctx
804
- })) 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
+ }
805
849
  return modified;
806
850
  }
807
851
  /**
@@ -897,7 +941,7 @@ var MongoEvaluationStore = class {
897
941
  if (tenantField && tenantValue !== void 0) update[tenantField] = tenantValue;
898
942
  await this.repo.Model.updateOne(filter, { $set: update }, {
899
943
  upsert: true,
900
- session
944
+ ...session !== void 0 ? { session } : {}
901
945
  });
902
946
  }
903
947
  async take(id, ctx) {
@@ -1049,23 +1093,37 @@ var EvaluationService = class {
1049
1093
  }
1050
1094
  async commit(evaluationId, orderId, ctx, options = {}) {
1051
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
+ };
1052
1107
  try {
1053
- return await this.unitOfWork.withTransaction(async (session) => {
1054
- const stored = await this.store.take(evaluationId, {
1055
- tenantValue: ctx.organizationId,
1056
- 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
1057
1116
  });
1058
- if (!stored) throw new EvaluationNotFoundError(evaluationId);
1059
- if (options.cartHash !== void 0 && options.cartHash !== stored.cartHash) throw new CartHashMismatchError(evaluationId);
1060
- snapshotForCapMapping = stored;
1061
- return await this.commitInTransaction(stored, evaluationId, orderId, session, ctx);
1062
1117
  });
1118
+ await flushPendingPromoEvents(this.dispatchDeps, pending);
1119
+ return result;
1063
1120
  } catch (err) {
1064
1121
  if (isWriteConflict(err) && snapshotForCapMapping) {
1065
1122
  const firstUsage = snapshotForCapMapping.programUsages[0];
1066
1123
  if (firstUsage) {
1124
+ const { session: _aborted, ...readCtx } = ctx;
1067
1125
  const program = await this.programRepo.getById(firstUsage.programId, {
1068
- ...ctx,
1126
+ ...repoOptionsFromCtx(readCtx),
1069
1127
  lean: true,
1070
1128
  throwOnNotFound: false
1071
1129
  });
@@ -1075,15 +1133,17 @@ var EvaluationService = class {
1075
1133
  throw err;
1076
1134
  }
1077
1135
  }
1078
- async commitInTransaction(stored, evaluationId, orderId, session, ctx) {
1136
+ async commitInTransaction(stored, evaluationId, orderId, txCtx) {
1137
+ const ctx = txCtx;
1138
+ const session = txCtx.session;
1079
1139
  for (const usage of stored.programUsages) {
1140
+ throwIfAborted(txCtx.signal);
1080
1141
  if (!await this.programRepo.tryIncrementUsage(usage.programId, {
1081
1142
  ...ctx,
1082
1143
  session
1083
1144
  })) {
1084
1145
  const program = await this.programRepo.getById(usage.programId, {
1085
- ...ctx,
1086
- session,
1146
+ ...repoOptionsFromCtx(ctx),
1087
1147
  lean: true,
1088
1148
  throwOnNotFound: false
1089
1149
  });
@@ -1114,21 +1174,37 @@ var EvaluationService = class {
1114
1174
  evaluationId,
1115
1175
  orderId,
1116
1176
  totalDiscount: stored.result.totalDiscount
1117
- }, ctx), {
1118
- ...ctx,
1119
- session
1120
- });
1177
+ }, txCtx), txCtx);
1121
1178
  return commitResult;
1122
1179
  }
1123
1180
  async rollback(evaluationId, ctx) {
1124
- await this.store.delete(evaluationId, { tenantValue: ctx.organizationId });
1181
+ await this.store.delete(evaluationId, {
1182
+ tenantValue: ctx.organizationId,
1183
+ session: ctx.session
1184
+ });
1125
1185
  await dispatchPromoEvent(this.dispatchDeps, createEvent$1(PromoEvents.EVALUATION_ROLLED_BACK, { evaluationId }, ctx), ctx);
1126
1186
  }
1127
1187
  async doEvaluate(input, ctx, isPreview) {
1128
- const submittedCodes = (input.codes ?? []).map((c) => c.toUpperCase());
1188
+ const submittedCodes = Array.from(new Set((input.codes ?? []).map((c) => c.trim().toUpperCase()).filter((c) => c.length > 0)));
1189
+ if (input.items.length === 0 || input.subtotal <= 0) return {
1190
+ evaluationId: randomBytes(16).toString("hex"),
1191
+ cartHash: computeCartHash(input),
1192
+ appliedDiscounts: [],
1193
+ freeProducts: [],
1194
+ totalDiscount: 0,
1195
+ subtotalAfterDiscount: Math.max(0, input.subtotal),
1196
+ appliedCodes: [],
1197
+ rejectedCodes: submittedCodes.map((code) => ({
1198
+ code,
1199
+ reason: "Cart is empty"
1200
+ })),
1201
+ warnings: [],
1202
+ isPreview,
1203
+ programsApplied: []
1204
+ };
1129
1205
  const programs = await this.programRepo.findActive(void 0, ctx);
1130
1206
  const programIds = programs.map((p) => p._id);
1131
- 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))]);
1132
1208
  const rulesByProgram = groupBy(allRules, (r) => r.programId);
1133
1209
  const rewardsByProgram = groupBy(allRewards, (r) => r.programId);
1134
1210
  const voucherMap = /* @__PURE__ */ new Map();
@@ -1152,6 +1228,7 @@ var EvaluationService = class {
1152
1228
  let exclusiveApplied = false;
1153
1229
  let stackableCount = 0;
1154
1230
  for (const program of programs) {
1231
+ throwIfAborted(ctx.signal);
1155
1232
  if (program.maxUsageTotal && program.usedCount >= program.maxUsageTotal) continue;
1156
1233
  if (!this.isCustomerEligible(program, input)) continue;
1157
1234
  if (program.maxUsagePerCustomer && input.customerId) {
@@ -1235,16 +1312,20 @@ var EvaluationService = class {
1235
1312
  programsApplied
1236
1313
  };
1237
1314
  if (!isPreview) {
1315
+ const { session: _liveSession, ...persistableCtx } = ctx;
1238
1316
  const snapshot = {
1239
1317
  result,
1240
- ctx,
1318
+ ctx: persistableCtx,
1241
1319
  customerId: input.customerId,
1242
1320
  programUsages,
1243
1321
  voucherUsages,
1244
1322
  cartHash,
1245
1323
  createdAt: /* @__PURE__ */ new Date()
1246
1324
  };
1247
- 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
+ });
1248
1329
  }
1249
1330
  await dispatchPromoEvent(this.dispatchDeps, createEvent$1(PromoEvents.EVALUATION_COMPLETED, {
1250
1331
  evaluationId,
@@ -1407,11 +1488,46 @@ var VoucherService = class {
1407
1488
  this.dispatchDeps = dispatchDeps;
1408
1489
  this.config = config;
1409
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
+ }
1410
1526
  async generateCodes(input, ctx) {
1411
1527
  const program = await this.programRepo.getById(input.programId, {
1412
1528
  throwOnNotFound: false,
1413
1529
  lean: true,
1414
- ...ctx
1530
+ ...repoOptionsFromCtx(ctx)
1415
1531
  });
1416
1532
  if (!program) throw new ProgramNotFoundError(input.programId);
1417
1533
  const { codeLength, codePrefix } = this.config.voucher;
@@ -1434,13 +1550,19 @@ var VoucherService = class {
1434
1550
  redemptions: [],
1435
1551
  ...input.metadata ? { metadata: input.metadata } : {}
1436
1552
  }));
1437
- 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
+ }
1438
1560
  await dispatchPromoEvent(this.dispatchDeps, createEvent$1(PromoEvents.VOUCHER_GENERATED, {
1439
1561
  programId: input.programId,
1440
1562
  voucherIds: vouchers.map((v) => v._id),
1441
1563
  codes: vouchers.map((v) => v.code),
1442
1564
  count: vouchers.length,
1443
- actorId: ctx.actorId
1565
+ actorRef: ctx.actorRef
1444
1566
  }, ctx), ctx);
1445
1567
  return vouchers;
1446
1568
  }
@@ -1448,7 +1570,7 @@ var VoucherService = class {
1448
1570
  const program = await this.programRepo.getById(input.programId, {
1449
1571
  throwOnNotFound: false,
1450
1572
  lean: true,
1451
- ...ctx
1573
+ ...repoOptionsFromCtx(ctx)
1452
1574
  });
1453
1575
  if (!program) throw new ProgramNotFoundError(input.programId);
1454
1576
  const code = input.code?.toUpperCase() ?? generateCode(this.config.voucher.codeLength, this.config.voucher.codePrefix);
@@ -1471,13 +1593,19 @@ var VoucherService = class {
1471
1593
  redemptions: [],
1472
1594
  ...input.metadata ? { metadata: input.metadata } : {}
1473
1595
  };
1474
- 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
+ }
1475
1603
  await dispatchPromoEvent(this.dispatchDeps, createEvent$1(PromoEvents.VOUCHER_GENERATED, {
1476
1604
  programId: input.programId,
1477
1605
  voucherIds: [voucher._id],
1478
1606
  codes: [voucher.code],
1479
1607
  count: 1,
1480
- actorId: ctx.actorId
1608
+ actorRef: ctx.actorRef
1481
1609
  }, ctx), ctx);
1482
1610
  return voucher;
1483
1611
  }
@@ -1510,7 +1638,7 @@ var VoucherService = class {
1510
1638
  const program = await this.programRepo.getById(voucher.programId, {
1511
1639
  throwOnNotFound: false,
1512
1640
  lean: true,
1513
- ...ctx
1641
+ ...repoOptionsFromCtx(ctx)
1514
1642
  });
1515
1643
  return {
1516
1644
  valid: true,
@@ -1526,18 +1654,12 @@ var VoucherService = class {
1526
1654
  };
1527
1655
  }
1528
1656
  async redeem(input, ctx) {
1529
- return this.unitOfWork.withTransaction(async (session) => {
1530
- const voucher = await this.voucherRepo.getByCode(input.code, {
1531
- ...ctx,
1532
- session
1533
- });
1657
+ return this.runTransactional(ctx, async (txCtx) => {
1658
+ const voucher = await this.voucherRepo.getByCode(input.code, txCtx);
1534
1659
  if (!voucher) throw new VoucherNotFoundError(input.code);
1535
1660
  this.assertVoucherUsable(voucher);
1536
1661
  if (input.idempotencyKey) {
1537
- if (await this.voucherRepo.hasIdempotencyKey(voucher._id, input.idempotencyKey, {
1538
- ...ctx,
1539
- session
1540
- })) throw new DuplicateRedemptionError(input.idempotencyKey);
1662
+ if (await this.voucherRepo.hasIdempotencyKey(voucher._id, input.idempotencyKey, txCtx)) throw new DuplicateRedemptionError(input.idempotencyKey);
1541
1663
  }
1542
1664
  const redemption = {
1543
1665
  orderId: input.orderId,
@@ -1547,14 +1669,10 @@ var VoucherService = class {
1547
1669
  ...input.idempotencyKey ? { idempotencyKey: input.idempotencyKey } : {},
1548
1670
  ...ctx.organizationId !== void 0 ? { organizationId: ctx.organizationId } : {}
1549
1671
  };
1550
- const updated = await this.voucherRepo.incrementUsage(voucher._id, redemption, {
1551
- ...ctx,
1552
- session
1553
- });
1672
+ const updated = await this.voucherRepo.incrementUsage(voucher._id, redemption, txCtx);
1554
1673
  if (updated.usedCount >= updated.usageLimit) await this.voucherRepo.update(voucher._id, { status: "used" }, {
1555
1674
  lean: true,
1556
- ...ctx,
1557
- session
1675
+ ...repoOptionsFromCtx(txCtx)
1558
1676
  });
1559
1677
  await dispatchPromoEvent(this.dispatchDeps, createEvent$1(PromoEvents.VOUCHER_REDEEMED, {
1560
1678
  voucherId: voucher._id,
@@ -1562,10 +1680,7 @@ var VoucherService = class {
1562
1680
  orderId: input.orderId,
1563
1681
  discountAmount: input.discountAmount,
1564
1682
  customerId: input.customerId
1565
- }, ctx), {
1566
- ...ctx,
1567
- session
1568
- });
1683
+ }, txCtx), txCtx);
1569
1684
  return updated;
1570
1685
  });
1571
1686
  }
@@ -1591,20 +1706,14 @@ var VoucherService = class {
1591
1706
  }
1592
1707
  }
1593
1708
  async spendInTransaction(input, ctx) {
1594
- return this.unitOfWork.withTransaction(async (session) => {
1595
- const voucher = await this.voucherRepo.getByCode(input.code, {
1596
- ...ctx,
1597
- session
1598
- });
1709
+ return this.runTransactional(ctx, async (txCtx) => {
1710
+ const voucher = await this.voucherRepo.getByCode(input.code, txCtx);
1599
1711
  if (!voucher) throw new VoucherNotFoundError(input.code);
1600
1712
  this.assertVoucherUsable(voucher);
1601
1713
  const balance = voucher.currentBalance ?? 0;
1602
1714
  if (!this.config.giftCard.allowNegativeBalance && balance < input.amount) throw new InsufficientBalanceError(input.code, balance, input.amount);
1603
1715
  if (input.idempotencyKey) {
1604
- if (await this.voucherRepo.hasIdempotencyKey(voucher._id, input.idempotencyKey, {
1605
- ...ctx,
1606
- session
1607
- })) throw new DuplicateRedemptionError(input.idempotencyKey);
1716
+ if (await this.voucherRepo.hasIdempotencyKey(voucher._id, input.idempotencyKey, txCtx)) throw new DuplicateRedemptionError(input.idempotencyKey);
1608
1717
  }
1609
1718
  const entry = {
1610
1719
  amount: -input.amount,
@@ -1614,10 +1723,7 @@ var VoucherService = class {
1614
1723
  ...input.idempotencyKey ? { idempotencyKey: input.idempotencyKey } : {},
1615
1724
  ...ctx.organizationId !== void 0 ? { organizationId: ctx.organizationId } : {}
1616
1725
  };
1617
- const updated = await this.voucherRepo.addLedgerEntry(voucher._id, entry, -input.amount, {
1618
- ...ctx,
1619
- session
1620
- });
1726
+ const updated = await this.voucherRepo.addLedgerEntry(voucher._id, entry, -input.amount, txCtx);
1621
1727
  const newBalance = updated.currentBalance ?? 0;
1622
1728
  await dispatchPromoEvent(this.dispatchDeps, createEvent$1(PromoEvents.GIFT_CARD_SPENT, {
1623
1729
  voucherId: voucher._id,
@@ -1625,24 +1731,17 @@ var VoucherService = class {
1625
1731
  amount: input.amount,
1626
1732
  remainingBalance: newBalance,
1627
1733
  orderId: input.orderId
1628
- }, ctx), {
1629
- ...ctx,
1630
- session
1631
- });
1734
+ }, txCtx), txCtx);
1632
1735
  if (newBalance <= 0) {
1633
1736
  await this.voucherRepo.update(voucher._id, { status: "used" }, {
1634
1737
  lean: true,
1635
- ...ctx,
1636
- session
1738
+ ...repoOptionsFromCtx(txCtx)
1637
1739
  });
1638
1740
  await dispatchPromoEvent(this.dispatchDeps, createEvent$1(PromoEvents.GIFT_CARD_EXHAUSTED, {
1639
1741
  voucherId: voucher._id,
1640
1742
  code: voucher.code,
1641
1743
  status: "used"
1642
- }, ctx), {
1643
- ...ctx,
1644
- session
1645
- });
1744
+ }, txCtx), txCtx);
1646
1745
  }
1647
1746
  return {
1648
1747
  code: updated.code,
@@ -1663,19 +1762,13 @@ var VoucherService = class {
1663
1762
  }
1664
1763
  }
1665
1764
  async topUpInTransaction(input, ctx) {
1666
- return this.unitOfWork.withTransaction(async (session) => {
1667
- const voucher = await this.voucherRepo.getByCode(input.code, {
1668
- ...ctx,
1669
- session
1670
- });
1765
+ return this.runTransactional(ctx, async (txCtx) => {
1766
+ const voucher = await this.voucherRepo.getByCode(input.code, txCtx);
1671
1767
  if (!voucher) throw new VoucherNotFoundError(input.code);
1672
1768
  const maxBalance = this.config.giftCard.maxBalance;
1673
1769
  if (maxBalance !== null && (voucher.currentBalance ?? 0) + input.amount > maxBalance) throw new ValidationError(`Top-up would exceed max balance of ${maxBalance}`);
1674
1770
  if (input.idempotencyKey) {
1675
- if (await this.voucherRepo.hasIdempotencyKey(voucher._id, input.idempotencyKey, {
1676
- ...ctx,
1677
- session
1678
- })) throw new DuplicateRedemptionError(input.idempotencyKey);
1771
+ if (await this.voucherRepo.hasIdempotencyKey(voucher._id, input.idempotencyKey, txCtx)) throw new DuplicateRedemptionError(input.idempotencyKey);
1679
1772
  }
1680
1773
  const entry = {
1681
1774
  amount: input.amount,
@@ -1684,24 +1777,17 @@ var VoucherService = class {
1684
1777
  ...input.idempotencyKey ? { idempotencyKey: input.idempotencyKey } : {},
1685
1778
  ...ctx.organizationId !== void 0 ? { organizationId: ctx.organizationId } : {}
1686
1779
  };
1687
- const updated = await this.voucherRepo.addLedgerEntry(voucher._id, entry, input.amount, {
1688
- ...ctx,
1689
- session
1690
- });
1780
+ const updated = await this.voucherRepo.addLedgerEntry(voucher._id, entry, input.amount, txCtx);
1691
1781
  if (voucher.status === "used") await this.voucherRepo.update(voucher._id, { status: "active" }, {
1692
1782
  lean: true,
1693
- ...ctx,
1694
- session
1783
+ ...repoOptionsFromCtx(txCtx)
1695
1784
  });
1696
1785
  await dispatchPromoEvent(this.dispatchDeps, createEvent$1(PromoEvents.GIFT_CARD_TOPPED_UP, {
1697
1786
  voucherId: voucher._id,
1698
1787
  code: voucher.code,
1699
1788
  amount: input.amount,
1700
1789
  newBalance: updated.currentBalance ?? 0
1701
- }, ctx), {
1702
- ...ctx,
1703
- session
1704
- });
1790
+ }, txCtx), txCtx);
1705
1791
  return {
1706
1792
  code: updated.code,
1707
1793
  initialBalance: updated.initialBalance ?? 0,
@@ -1734,229 +1820,6 @@ function createServices(deps) {
1734
1820
  };
1735
1821
  }
1736
1822
  //#endregion
1737
- //#region src/events/promo-event-catalog.ts
1738
- function definePromoEvent(input) {
1739
- const { name, version = 1, description, zodSchema } = input;
1740
- const def = {
1741
- name,
1742
- version,
1743
- schema: z.toJSONSchema(zodSchema),
1744
- zodSchema,
1745
- create(payload, meta) {
1746
- return createEvent(name, payload, {
1747
- source: "promo",
1748
- ...meta
1749
- });
1750
- }
1751
- };
1752
- if (description !== void 0) def.description = description;
1753
- return def;
1754
- }
1755
- /** Mirrors `ProgramLifecyclePayload`. */
1756
- const programLifecycleSchema = z.object({
1757
- programId: z.string(),
1758
- programType: z.string(),
1759
- status: z.string(),
1760
- actorId: z.string().optional()
1761
- });
1762
- /** Mirrors `RulePayload`. */
1763
- const ruleSchema = z.object({
1764
- programId: z.string(),
1765
- ruleId: z.string(),
1766
- actorId: z.string().optional()
1767
- });
1768
- /** Mirrors `RewardPayload`. */
1769
- const rewardSchema = z.object({
1770
- programId: z.string(),
1771
- rewardId: z.string(),
1772
- actorId: z.string().optional()
1773
- });
1774
- /** Mirrors `VoucherGeneratedPayload`. */
1775
- const voucherGeneratedSchema = z.object({
1776
- programId: z.string(),
1777
- voucherIds: z.array(z.string()),
1778
- codes: z.array(z.string()),
1779
- count: z.number().int().nonnegative(),
1780
- actorId: z.string().optional()
1781
- });
1782
- /** Mirrors `VoucherRedeemedPayload`. */
1783
- const voucherRedeemedSchema = z.object({
1784
- voucherId: z.string(),
1785
- code: z.string(),
1786
- orderId: z.string(),
1787
- discountAmount: z.number(),
1788
- customerId: z.string().optional()
1789
- });
1790
- /**
1791
- * Mirrors `VoucherLifecyclePayload` — shared by VOUCHER_CANCELLED,
1792
- * VOUCHER_EXPIRED, and GIFT_CARD_EXHAUSTED (repo emits `status: 'cancelled'`
1793
- * / `'used'` / host-supplied terminal value).
1794
- */
1795
- const voucherLifecycleSchema = z.object({
1796
- voucherId: z.string(),
1797
- code: z.string(),
1798
- status: z.string()
1799
- });
1800
- /** Mirrors `GiftCardSpentPayload`. */
1801
- const giftCardSpentSchema = z.object({
1802
- voucherId: z.string(),
1803
- code: z.string(),
1804
- amount: z.number(),
1805
- remainingBalance: z.number(),
1806
- orderId: z.string()
1807
- });
1808
- /** Mirrors `GiftCardToppedUpPayload`. */
1809
- const giftCardToppedUpSchema = z.object({
1810
- voucherId: z.string(),
1811
- code: z.string(),
1812
- amount: z.number(),
1813
- newBalance: z.number()
1814
- });
1815
- /** Mirrors `EvaluationCompletedPayload`. */
1816
- const evaluationCompletedSchema = z.object({
1817
- evaluationId: z.string(),
1818
- totalDiscount: z.number(),
1819
- programsApplied: z.number().int().nonnegative(),
1820
- codesUsed: z.array(z.string()),
1821
- isPreview: z.boolean()
1822
- });
1823
- /** Mirrors `EvaluationCommittedPayload`. */
1824
- const evaluationCommittedSchema = z.object({
1825
- evaluationId: z.string(),
1826
- orderId: z.string(),
1827
- totalDiscount: z.number()
1828
- });
1829
- /** Single-field rollback payload — emitted by `evaluation.service.ts`. */
1830
- const evaluationRolledBackSchema = z.object({ evaluationId: z.string() });
1831
- const ProgramCreated = definePromoEvent({
1832
- name: PromoEvents.PROGRAM_CREATED,
1833
- description: "A new promo program was created (starts in draft).",
1834
- zodSchema: programLifecycleSchema
1835
- });
1836
- const ProgramActivated = definePromoEvent({
1837
- name: PromoEvents.PROGRAM_ACTIVATED,
1838
- description: "A draft or paused program was activated.",
1839
- zodSchema: programLifecycleSchema
1840
- });
1841
- const ProgramPaused = definePromoEvent({
1842
- name: PromoEvents.PROGRAM_PAUSED,
1843
- description: "An active program was paused.",
1844
- zodSchema: programLifecycleSchema
1845
- });
1846
- const ProgramArchived = definePromoEvent({
1847
- name: PromoEvents.PROGRAM_ARCHIVED,
1848
- description: "A program was archived (terminal — no further transitions).",
1849
- zodSchema: programLifecycleSchema
1850
- });
1851
- const RuleAdded = definePromoEvent({
1852
- name: PromoEvents.RULE_ADDED,
1853
- description: "A new rule was added to a program.",
1854
- zodSchema: ruleSchema
1855
- });
1856
- const RuleUpdated = definePromoEvent({
1857
- name: PromoEvents.RULE_UPDATED,
1858
- description: "An existing rule on a program was updated.",
1859
- zodSchema: ruleSchema
1860
- });
1861
- const RuleRemoved = definePromoEvent({
1862
- name: PromoEvents.RULE_REMOVED,
1863
- description: "A rule was removed from a program.",
1864
- zodSchema: ruleSchema
1865
- });
1866
- const RewardAdded = definePromoEvent({
1867
- name: PromoEvents.REWARD_ADDED,
1868
- description: "A reward was added to a program.",
1869
- zodSchema: rewardSchema
1870
- });
1871
- const RewardUpdated = definePromoEvent({
1872
- name: PromoEvents.REWARD_UPDATED,
1873
- description: "A reward on a program was updated.",
1874
- zodSchema: rewardSchema
1875
- });
1876
- const RewardRemoved = definePromoEvent({
1877
- name: PromoEvents.REWARD_REMOVED,
1878
- description: "A reward was removed from a program.",
1879
- zodSchema: rewardSchema
1880
- });
1881
- const VoucherGenerated = definePromoEvent({
1882
- name: PromoEvents.VOUCHER_GENERATED,
1883
- description: "One or more vouchers were generated for a program.",
1884
- zodSchema: voucherGeneratedSchema
1885
- });
1886
- const VoucherRedeemed = definePromoEvent({
1887
- name: PromoEvents.VOUCHER_REDEEMED,
1888
- description: "A voucher was redeemed against an order.",
1889
- zodSchema: voucherRedeemedSchema
1890
- });
1891
- const VoucherCancelled = definePromoEvent({
1892
- name: PromoEvents.VOUCHER_CANCELLED,
1893
- description: "A voucher was cancelled by an operator.",
1894
- zodSchema: voucherLifecycleSchema
1895
- });
1896
- const VoucherExpired = definePromoEvent({
1897
- name: PromoEvents.VOUCHER_EXPIRED,
1898
- description: "A voucher reached its expiry date and was transitioned to expired.",
1899
- zodSchema: voucherLifecycleSchema
1900
- });
1901
- const GiftCardSpent = definePromoEvent({
1902
- name: PromoEvents.GIFT_CARD_SPENT,
1903
- description: "A gift-card voucher was debited against an order — remaining balance reported.",
1904
- zodSchema: giftCardSpentSchema
1905
- });
1906
- const GiftCardToppedUp = definePromoEvent({
1907
- name: PromoEvents.GIFT_CARD_TOPPED_UP,
1908
- description: "A gift-card voucher received a top-up — new balance reported.",
1909
- zodSchema: giftCardToppedUpSchema
1910
- });
1911
- const GiftCardExhausted = definePromoEvent({
1912
- name: PromoEvents.GIFT_CARD_EXHAUSTED,
1913
- description: "A gift-card voucher reached a zero balance and was marked used.",
1914
- zodSchema: voucherLifecycleSchema
1915
- });
1916
- const EvaluationCompleted = definePromoEvent({
1917
- name: PromoEvents.EVALUATION_COMPLETED,
1918
- description: "A cart evaluation finished (preview or pre-commit) — totals and applied codes reported.",
1919
- zodSchema: evaluationCompletedSchema
1920
- });
1921
- const EvaluationCommitted = definePromoEvent({
1922
- name: PromoEvents.EVALUATION_COMMITTED,
1923
- description: "A stored evaluation was committed against an order — usage counters moved.",
1924
- zodSchema: evaluationCommittedSchema
1925
- });
1926
- const EvaluationRolledBack = definePromoEvent({
1927
- name: PromoEvents.EVALUATION_ROLLED_BACK,
1928
- description: "A stored evaluation was discarded without commit.",
1929
- zodSchema: evaluationRolledBackSchema
1930
- });
1931
- /**
1932
- * Every promo event defined in the package — pass to Arc's
1933
- * `EventRegistry`. Hosts wire ONE array; the whole `promo.*` namespace
1934
- * becomes introspectable via OpenAPI and auto-validated at publish time
1935
- * when `eventPlugin({ validateMode: 'reject' })` is set.
1936
- */
1937
- const promoEventDefinitions = [
1938
- ProgramCreated,
1939
- ProgramActivated,
1940
- ProgramPaused,
1941
- ProgramArchived,
1942
- RuleAdded,
1943
- RuleUpdated,
1944
- RuleRemoved,
1945
- RewardAdded,
1946
- RewardUpdated,
1947
- RewardRemoved,
1948
- VoucherGenerated,
1949
- VoucherRedeemed,
1950
- VoucherCancelled,
1951
- VoucherExpired,
1952
- GiftCardSpent,
1953
- GiftCardToppedUp,
1954
- GiftCardExhausted,
1955
- EvaluationCompleted,
1956
- EvaluationCommitted,
1957
- EvaluationRolledBack
1958
- ];
1959
- //#endregion
1960
1823
  //#region src/index.ts
1961
1824
  function resolveConfig(config) {
1962
1825
  const tenant = resolveTenantConfig(config.tenant);
@@ -1996,14 +1859,30 @@ function resolveConfig(config) {
1996
1859
  * that wires it. See packages/mongokit/src/transaction.ts.
1997
1860
  */
1998
1861
  var MongoUnitOfWork = class {
1999
- constructor(connection) {
1862
+ constructor(connection, allowFallback = false) {
2000
1863
  this.connection = connection;
1864
+ this.allowFallback = allowFallback;
2001
1865
  }
2002
1866
  withTransaction(cb) {
2003
- return withTransaction(this.connection, cb);
1867
+ return withTransaction(this.connection, cb, this.allowFallback ? { allowFallback: true } : {});
2004
1868
  }
2005
1869
  };
2006
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
+ /**
2007
1886
  * Build the promo engine for a host application.
2008
1887
  *
2009
1888
  * **Index management — important for boot performance:**
@@ -2042,7 +1921,12 @@ var MongoUnitOfWork = class {
2042
1921
  */
2043
1922
  function createPromoEngine(config) {
2044
1923
  const resolvedConfig = resolveConfig(config);
2045
- 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
+ });
2046
1930
  const events = config.events?.transport ?? new InProcessPromoBus();
2047
1931
  const dispatchDeps = {
2048
1932
  events,
@@ -2050,12 +1934,14 @@ function createPromoEngine(config) {
2050
1934
  ...config.logger !== void 0 ? { logger: config.logger } : {}
2051
1935
  };
2052
1936
  const repositories = createRepositories(models, config.plugins, resolvedConfig.tenant, dispatchDeps);
1937
+ const allowNonTransactional = config.allowNonTransactional ?? false;
1938
+ if (!allowNonTransactional) assertPromoCapabilities(repositories.program.capabilities);
2053
1939
  return {
2054
1940
  models,
2055
1941
  repositories,
2056
1942
  services: createServices({
2057
1943
  repositories,
2058
- unitOfWork: new MongoUnitOfWork(config.mongoose),
1944
+ unitOfWork: new MongoUnitOfWork(config.mongoose, allowNonTransactional),
2059
1945
  dispatchDeps,
2060
1946
  config: resolvedConfig,
2061
1947
  ...config.evaluationStore !== void 0 ? { evaluationStore: config.evaluationStore } : {}
@@ -2067,4 +1953,4 @@ function createPromoEngine(config) {
2067
1953
  };
2068
1954
  }
2069
1955
  //#endregion
2070
- 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 };