@classytic/promo 0.2.3 → 0.2.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -3,6 +3,59 @@
3
3
  Format based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
4
4
  adhering to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
5
5
 
6
+ ## [0.2.5] — 2026-05-08
7
+
8
+ ### Fixed
9
+
10
+ - **§P8 dispatch — `outbox.save` failures now propagate.** [src/events/dispatch.ts](src/events/dispatch.ts)
11
+ previously caught `outbox.save` errors and only logged them, swallowing
12
+ the failure. Per PACKAGE_RULES §P8 the outbox row is the durability
13
+ anchor for the transactional-outbox pattern: if the save fails, the
14
+ caller's transaction MUST roll back so the business doc and the event
15
+ row land atomically. Swallowing meant a parent doc could commit while
16
+ the event vanished, with no relay row to recover from. Fix re-throws
17
+ after logging, matching the textbook §P8 shape used in
18
+ `@classytic/loyalty` 0.2.3 and `@classytic/order`.
19
+ - `events.publish` failures continue to be swallowed (correctly, per §P8 —
20
+ the host's outbox relay re-publishes from the durable row), with logging
21
+ preserved for observability.
22
+
23
+ ### Changed
24
+
25
+ - **Peer-dep ranges tightened** — `@classytic/mongokit` `>=3.13.3`
26
+ (was `>=3.13.0`) and `@classytic/repo-core` `>=0.4.2` (was `>=0.4.0`).
27
+ 3.13.3 preserves `errorLabels` on `MongoServerError` so concurrent
28
+ transactional writes (the `claim()`-based program / voucher state
29
+ transitions) retry correctly under contention; 0.4.2 adds the typed
30
+ `FindAllOptions` shape with optional `limit?` for callers that need
31
+ bounded non-paginated reads.
32
+
33
+ ### Audited (no change required)
34
+
35
+ - Two `Model.*` raw-driver sites in
36
+ [src/repositories/pending-evaluation.repository.ts](src/repositories/pending-evaluation.repository.ts)
37
+ (`findOneAndDelete`, `deleteOne`) confirmed as the documented atomic
38
+ take-and-return semantic that mongokit's `delete()` doesn't expose.
39
+ Tenant scoping is re-implemented at the repository layer (mirrors
40
+ `multiTenantPlugin`'s injection) so the bypass keeps cross-tenant
41
+ isolation intact.
42
+ - `customIdPlugin` not used — §P9 dual-id resolution N/A.
43
+
44
+ ## [0.2.4]
45
+
46
+ ### Fixed
47
+
48
+ - **`evaluation.preview` / `evaluation.evaluate`**: submitted codes are now
49
+ trimmed (in addition to uppercased) before resolution. `' bigboss10 '`
50
+ resolves to `'BIGBOSS10'` and is reported under that canonical form in
51
+ `appliedCodes`. Duplicates after normalization are de-duped (a single code
52
+ submitted multiple times appears once).
53
+ - **Empty-cart preview**: when `items` is empty (or `subtotal <= 0`), the
54
+ engine now rejects all submitted codes with `reason: 'Cart is empty'`
55
+ instead of listing them under `appliedCodes` with `totalDiscount: 0`. Hosts
56
+ that surface `appliedCodes` directly to the UI no longer need to special-case
57
+ zero-item carts.
58
+
6
59
  ## [0.2.0]
7
60
 
8
61
  Structural rewrite on top of the 0.1.0 shape. The public API is now a
package/dist/index.mjs CHANGED
@@ -501,6 +501,7 @@ async function dispatchPromoEvent(deps, event, ctx) {
501
501
  await deps.outbox.save(event, saveOptions);
502
502
  } catch (err) {
503
503
  logger.error(`[promo] outbox.save failed for ${event.type}:`, err);
504
+ throw err;
504
505
  }
505
506
  if (deps.events) try {
506
507
  await deps.events.publish(event);
@@ -1125,7 +1126,23 @@ var EvaluationService = class {
1125
1126
  await dispatchPromoEvent(this.dispatchDeps, createEvent$1(PromoEvents.EVALUATION_ROLLED_BACK, { evaluationId }, ctx), ctx);
1126
1127
  }
1127
1128
  async doEvaluate(input, ctx, isPreview) {
1128
- const submittedCodes = (input.codes ?? []).map((c) => c.toUpperCase());
1129
+ const submittedCodes = Array.from(new Set((input.codes ?? []).map((c) => c.trim().toUpperCase()).filter((c) => c.length > 0)));
1130
+ if (input.items.length === 0 || input.subtotal <= 0) return {
1131
+ evaluationId: randomBytes(16).toString("hex"),
1132
+ cartHash: computeCartHash(input),
1133
+ appliedDiscounts: [],
1134
+ freeProducts: [],
1135
+ totalDiscount: 0,
1136
+ subtotalAfterDiscount: Math.max(0, input.subtotal),
1137
+ appliedCodes: [],
1138
+ rejectedCodes: submittedCodes.map((code) => ({
1139
+ code,
1140
+ reason: "Cart is empty"
1141
+ })),
1142
+ warnings: [],
1143
+ isPreview,
1144
+ programsApplied: []
1145
+ };
1129
1146
  const programs = await this.programRepo.findActive(void 0, ctx);
1130
1147
  const programIds = programs.map((p) => p._id);
1131
1148
  const [allRules, allRewards] = await Promise.all([this.ruleRepo.findAll({ programId: { $in: programIds } }, ctx), this.rewardRepo.findAll({ programId: { $in: programIds } }, ctx)]);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@classytic/promo",
3
- "version": "0.2.3",
3
+ "version": "0.2.5",
4
4
  "description": "Production-grade promotion, coupon, and discount engine for MongoDB — programs, rules, rewards, vouchers, gift cards, buy-x-get-y",
5
5
  "author": "Classytic",
6
6
  "homepage": "https://www.npmjs.com/package/@classytic/promo",
@@ -57,18 +57,18 @@
57
57
  "release": "npm run push -- main && npm run release:tag && npm publish"
58
58
  },
59
59
  "peerDependencies": {
60
- "@classytic/mongokit": ">=3.13.0",
60
+ "@classytic/mongokit": ">=3.13.3",
61
61
  "@classytic/primitives": ">=0.5.0",
62
- "@classytic/repo-core": ">=0.4.0",
62
+ "@classytic/repo-core": ">=0.4.2",
63
63
  "mongoose": ">=9.4.1",
64
64
  "zod": ">=4.0.0"
65
65
  },
66
66
  "devDependencies": {
67
67
  "@biomejs/biome": "^2.4.9",
68
68
  "@classytic/dev-tools": "^0.2.0",
69
- "@classytic/mongokit": ">=3.13.0",
69
+ "@classytic/mongokit": "^3.13.3",
70
70
  "@classytic/primitives": ">=0.5.0",
71
- "@classytic/repo-core": ">=0.4.0",
71
+ "@classytic/repo-core": "^0.4.2",
72
72
  "@types/node": "^25.5.0",
73
73
  "@vitest/coverage-v8": "^3.2.4",
74
74
  "knip": "^6.3.0",