@classytic/loyalty 0.4.0 → 0.5.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/CHANGELOG.md CHANGED
@@ -1,215 +1,231 @@
1
- # Changelog
2
-
3
- Format based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
4
- adhering to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
5
-
6
- ## [0.4.0] — addendum (2026-07-05)
7
-
8
- ### Changed (behavioral)
9
-
10
- - **§P8.1 relay-only session guard** — `UnmanagedSessionError` now throws
11
- only when a host session arrives with NEITHER a `[PENDING_EVENTS]`
12
- queue NOR an outbox. Session + outbox + no queue is legal
13
- durable-relay-only mode: the row commits with the host transaction, the
14
- relay delivers post-commit, the immediate transport publish is skipped.
15
- (Previously the guard was stricter than spec and threw even with an
16
- outbox wired.) Aligns loyalty with invoice / catalog / order /
17
- purchase / transfer / promo / payrun.
18
- - **Stack floors raised** — peers `@classytic/mongokit >=3.18.0`,
19
- `@classytic/repo-core >=0.7.0`; full suite verified against those
20
- versions.
21
-
22
- ## [0.4.0] — 2026-07-03
23
-
24
- ### Fixed
25
-
26
- - **`evaluateAction` is now idempotent per recurrence key.** The order path
27
- dedupes with `${orderId}:${ruleId}`, but the action path (signup /
28
- referral / birthday / review) never set an `idempotencyKey` — a retried
29
- sweep or double-fired event double-awarded points. When `referenceId` is
30
- supplied, each rule award now carries `${referenceId}:${ruleId}`; callers
31
- encode the recurrence period in `referenceId` (e.g.
32
- `birthday:2026:<memberId>`) and the unique partial index on
33
- `idempotencyKey` becomes the authoritative once-only guard.
34
- - **Action rules honor `conditions` now.** `evaluateAction` skipped
35
- `matchesConditions` entirely, silently ignoring admin-authored
36
- `tiers` / `dateRange` / `dayOfWeek` on action-type rules ("only gold
37
- members get birthday points" did nothing). Conditions now apply;
38
- `minOrderAmount` is skipped for actions (no order to measure).
39
- - **`allowNonTransactional` now actually works at runtime.** The flag
40
- silenced the boot capability gate but `MongoUnitOfWork.withTransaction`
41
- still opened a real transaction unconditionally — on standalone Mongo
42
- every `earnPoints`/`redeem` unit failed with "does not support retryable
43
- writes" despite the documented escape hatch. The unit of work now
44
- delegates to mongokit's `withTransaction` with `allowFallback` so the
45
- transactions-unsupported error re-runs the unit on a plain session
46
- (partial-write risk accepted, exactly what the flag opts into).
47
- - Day arithmetic (tier-downgrade grace expiry, referral rate-limit window)
48
- now uses `addDays` from `@classytic/primitives/calendar` — exact 24h × n
49
- durations instead of local `setDate`, which shifted with the deploy
50
- machine's `TZ` env. Primitives peer bumped to `>=0.9.0`.
51
-
52
- ## [0.3.0] — 2026-06-13
53
-
54
- Stack upgrade to mongokit 3.16 / repo-core 0.6 / primitives 0.7.2 plus
55
- verified correctness + PACKAGE_RULES compliance fixes. No new features.
56
-
57
- ### Fixed
58
-
59
- - **Stack actually resolves the new floor now (CRITICAL).** The committed
60
- lockfile silently served `@classytic/mongokit@3.13.0` +
61
- `@classytic/repo-core@0.4.0` despite the `^3.13.3` / `^0.4.2` pins, so the
62
- whole test suite ran against the OLD stack. Clean reinstall + peer/dev bumps
63
- pin the resolved tree to mongokit **3.16.0**, repo-core **0.6.0**, primitives
64
- **0.7.2** (verified with `npm ls`).
65
- - **§P8 outbox is now session-bound on the transactional verbs.** `earnPoints`,
66
- `adjustPoints`, `reserve`, `confirm`, `release`, and `reverseConfirmed`
67
- previously dispatched the event AFTER `withTransaction` committed, so
68
- `outbox.save` ran OUTSIDE the unit of work — the business doc could commit
69
- while the durable event row was lost (or vice-versa). Dispatch now runs
70
- INSIDE the transaction (outbox row commits atomically with the write) and the
71
- transport publish is deferred to a post-commit flush (`runLoyaltyUnit` +
72
- `[PENDING_EVENTS]`), so in-process subscribers never see a state that may roll
73
- back. A new integration test asserts the save is session-bound and that a
74
- failing `outbox.save` rolls the `$inc` balance write back.
75
- - **Redemption slot no longer permanently blocked after release.** The
76
- `{ ownerType, ownerId }` unique index spanned ALL statuses, so a released /
77
- reversed reservation E11000-blocked any retry on the same order (payment
78
- retry, cart re-attempt) forever. It is now a **partial** unique index scoped
79
- to active statuses (`reserved`, `confirmed`) — terminal states free the slot
80
- (PACKAGE_RULES rule 32/38/39). Re-reserve-after-release and second-active-
81
- reserve-blocked tests added.
82
-
83
- ### Added
84
-
85
- - **`UnmanagedSessionError` guard (§P8).** When a host passes `ctx.session`
86
- (it owns the outer transaction) without attaching a `[PENDING_EVENTS]` queue,
87
- dispatch throws BEFORE any side effect instead of publishing / saving outside
88
- the host's unit of work (which would leak ghost events on rollback).
89
- `PENDING_EVENTS` and `flushPendingLoyaltyEvents` are exported for hosts that
90
- manage their own transaction.
91
- - **Capability boot gate.** `createLoyaltyEngine()` asserts the backend declares
92
- the `transactions` capability (repo-core 0.6 / mongokit 3.16+) at engine
93
- creation — fail loud at boot rather than with a cryptic driver error on the
94
- first transactional call. `allowNonTransactional: true` opts out for local-dev
95
- on standalone Mongo. New `LoyaltyCapabilityError`.
96
- - **Cooperative cancellation in cron loops.** `processExpirations`, `awardBatch`,
97
- `cleanupExpired` (redemption + referral), `evaluateAll`, and
98
- `sweepExpiredOverrides` check `ctx.signal` via `throwIfAborted` between items —
99
- abortable batches; each per-item write is its own committed unit of work, so
100
- aborting never rolls back already-processed rows. `LoyaltyContext` gained
101
- `signal?: AbortSignal`.
102
-
103
- ### Changed
104
-
105
- - **Domain errors implement `HttpError`** from `@classytic/repo-core/errors`
106
- (was a bespoke `httpStatus` shape). Clean break: `httpStatus` → `status`,
107
- and codes are now hierarchical + lowercase (`loyalty.<entity>.<problem>`,
108
- e.g. `loyalty.member.not_found`, `loyalty.points.insufficient`) so arc
109
- serializes them to `ErrorContract` correctly. No deprecation alias.
110
- - **Peer-dep ranges raised** — `@classytic/mongokit >=3.16.0`,
111
- `@classytic/repo-core >=0.6.0` (`>=` style). devDeps bumped to match and
112
- primitives devDep pinned to `^0.7.2`.
113
- - **`exactOptionalPropertyTypes: true`** enabled in tsconfig (mandatory per
114
- PACKAGE_RULES Build Hygiene / P10); fixed the three pre-existing optional-vs-
115
- undefined violations it surfaced.
116
- - **tsconfig no longer emits sourcemaps or declaration maps**
117
- (`sourceMap: false`, `declarationMap: false`) — both leaked source. tsdown
118
- was already correct; tsconfig now matches.
119
-
120
- ## [0.2.3] — 2026-05-08
121
-
122
- ### Fixed
123
-
124
- - **§P8 dispatch — `outbox.save` failures now propagate.** [src/events/dispatch.ts](src/events/dispatch.ts)
125
- previously caught `outbox.save` errors and swallowed them silently. Per
126
- PACKAGE_RULES §P8 the outbox row is the durability anchor for the
127
- transactional-outbox pattern: if the save fails, the caller's transaction
128
- MUST roll back so the business doc and the event row land atomically.
129
- Swallowing the failure meant a parent doc could commit while the event
130
- vanished, with no relay row to recover from. Fix re-throws after logging.
131
- `events.publish` failures continue to be swallowed (correctly, per §P8 —
132
- the host's outbox relay re-publishes from the durable row), but now log
133
- before swallowing so failures are observable.
134
- - **`DispatchDeps.logger` field added.** Optional. Both arms of the
135
- dispatch helper log through it when set, surfacing failures in the host's
136
- observability stack without coupling the package to a specific logger.
137
-
138
- ### Changed
139
-
140
- - **Peer-dep ranges tightened** — `@classytic/mongokit` `>=3.13.3`
141
- (was `>=3.13.0`) and `@classytic/repo-core` `>=0.4.2` (was `>=0.4.0`).
142
- 3.13.3 preserves `errorLabels` on `MongoServerError` so concurrent
143
- transactional writes (the `claim()`-based redemption + tier-eval paths)
144
- retry correctly under contention; 0.4.2 adds the typed `FindAllOptions`
145
- shape with `limit?` for callers that need a bounded non-paginated read.
146
-
147
- ### Internal
148
-
149
- - Replaced 4 `arr.find()!` non-null assertions in [src/index.ts](src/index.ts)
150
- + scenario tests with explicit narrowing — TS now proves what the runtime
151
- shape already guaranteed.
152
-
153
- ## [0.2.0]
154
-
155
- Structural rewrite on top of the 0.1.0 shape. The public API is now a
156
- smaller, sharper surface organized around seven subpath exports
157
- (`/domain/enums`, `/models`, `/repositories`, `/events`, `/types`,
158
- `/notifications`, plus the root engine factory). Legacy barrel + service
159
- layers were removed; hosts wire business flows directly against the
160
- repositories and the engine factory.
161
-
162
- ### Changed
163
-
164
- - **Domain restructure.** Dropped the `src/domain/ports/` + `src/services/`
165
- layer in favor of thin repositories that expose exactly the operations
166
- hosts need — `earn`, `redeem`, `adjust`, `expire`, referral credit,
167
- tier recomputation. No more `earning.service` / `redemption.service` /
168
- `tier.service` / `referral.service` / `ledger.service` / `member.service`
169
- / `member-guard` indirections; the equivalent logic lives on the
170
- repositories where it's naturally atomic.
171
- - **Events subsystem simplified.** Removed `events/event-bus.ts` +
172
- `events/event-types.ts` (both are internal indirection that the engine
173
- didn't need). Event payloads are now colocated with the repository
174
- that emits them.
175
- - **Repositories updated to mongokit 3.11 + repo-core 0.2 conventions.**
176
- Every repo extends `Repository<T>` directly, uses the tenant plugin via
177
- `injectTenant`, and atomic state changes go through CAS guards.
178
- - **Peer deps** unchanged in spirit, tightened in range:
179
- - `@classytic/mongokit >= 3.11`
180
- - `@classytic/primitives >= 0.1`
181
- - `@classytic/repo-core >= 0.2`
182
- - `mongoose >= 9.4`, `zod >= 4`
183
- - **DevDeps:** `@classytic/primitives` devDep moved from `file:` to `>=0.1.0`
184
- (now that primitives ships on npm); all `@classytic/*` dev ranges
185
- standardized on `>=`.
186
-
187
- ### Removed
188
-
189
- - `src/services/*` — logic folded into repositories.
190
- - `src/domain/ports/*` barrels and per-port files — structural types
191
- now live with the repositories they describe.
192
- - Event-bus indirection — repositories emit via the engine's configured
193
- transport directly.
194
-
195
- ### Added
196
-
197
- - Subpath export `/notifications` — notification port + built-in
198
- lifecycle payloads (tier up/down, points expiring, referral credit).
199
- - Test restructure — 257 tests across unit + integration projects; the
200
- deleted legacy suites were collapsed into focused scenario files that
201
- exercise the new public API.
202
-
203
- ### Peer compatibility
204
-
205
- Anyone on 0.1.0 wiring against the old `*.service.ts` modules will need
206
- to migrate to the repository methods. Signatures are similar — earn /
207
- redeem / adjust remain the nouns — but the call sites move from
208
- `services.earning.earnPoints(...)` to `repositories.pointTransaction.earn(...)`
209
- and similar. No behavior changes in the ledger semantics.
210
-
211
- ## [0.1.0]
212
-
213
- Initial release. Loyalty points, tiers, earning rules, referrals, and
214
- redemption engine for MongoDB. Tenant-scoped, event-emitting, atomic
215
- point-ledger with CAS guards.
1
+ # Changelog
2
+
3
+ Format based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
4
+ adhering to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
5
+
6
+ ## [0.5.0] — unpublished (2026-07-15)
7
+
8
+ ### Changed — mongokit 3.22 adopted (peer floors: mongokit >=3.22.0, repo-core >=0.13.0, primitives >=0.11.0)
9
+
10
+ - **Points ledger fence enforced** — `PointTransaction` rows are the
11
+ balance audit trail; `appendOnlyPlugin` now refuses every mutation
12
+ path (405 `APPEND_ONLY_VIOLATION`). Previously nothing enforced it —
13
+ a stray `update()` could rewrite an earn/burn row and silently
14
+ desync the audit trail from member balances. The expiry sweeper's
15
+ `expiredAt` stamp is the one engine-owned lifecycle write and passes
16
+ the audited `bypassAppendOnly` escape.
17
+ - No FSM boilerplate existed (no assert+claim sites, no
18
+ read-modify-write history) — loyalty was already clean there; this
19
+ release is the fence + kit refresh (clean registry install, no
20
+ legacy flags).
21
+
22
+ ## [0.4.0] — addendum (2026-07-05)
23
+
24
+ ### Changed (behavioral)
25
+
26
+ - **§P8.1 relay-only session guard** — `UnmanagedSessionError` now throws
27
+ only when a host session arrives with NEITHER a `[PENDING_EVENTS]`
28
+ queue NOR an outbox. Session + outbox + no queue is legal
29
+ durable-relay-only mode: the row commits with the host transaction, the
30
+ relay delivers post-commit, the immediate transport publish is skipped.
31
+ (Previously the guard was stricter than spec and threw even with an
32
+ outbox wired.) Aligns loyalty with invoice / catalog / order /
33
+ purchase / transfer / promo / payrun.
34
+ - **Stack floors raised** — peers `@classytic/mongokit >=3.18.0`,
35
+ `@classytic/repo-core >=0.7.0`; full suite verified against those
36
+ versions.
37
+
38
+ ## [0.4.0] — 2026-07-03
39
+
40
+ ### Fixed
41
+
42
+ - **`evaluateAction` is now idempotent per recurrence key.** The order path
43
+ dedupes with `${orderId}:${ruleId}`, but the action path (signup /
44
+ referral / birthday / review) never set an `idempotencyKey` — a retried
45
+ sweep or double-fired event double-awarded points. When `referenceId` is
46
+ supplied, each rule award now carries `${referenceId}:${ruleId}`; callers
47
+ encode the recurrence period in `referenceId` (e.g.
48
+ `birthday:2026:<memberId>`) and the unique partial index on
49
+ `idempotencyKey` becomes the authoritative once-only guard.
50
+ - **Action rules honor `conditions` now.** `evaluateAction` skipped
51
+ `matchesConditions` entirely, silently ignoring admin-authored
52
+ `tiers` / `dateRange` / `dayOfWeek` on action-type rules ("only gold
53
+ members get birthday points" did nothing). Conditions now apply;
54
+ `minOrderAmount` is skipped for actions (no order to measure).
55
+ - **`allowNonTransactional` now actually works at runtime.** The flag
56
+ silenced the boot capability gate but `MongoUnitOfWork.withTransaction`
57
+ still opened a real transaction unconditionally — on standalone Mongo
58
+ every `earnPoints`/`redeem` unit failed with "does not support retryable
59
+ writes" despite the documented escape hatch. The unit of work now
60
+ delegates to mongokit's `withTransaction` with `allowFallback` so the
61
+ transactions-unsupported error re-runs the unit on a plain session
62
+ (partial-write risk accepted, exactly what the flag opts into).
63
+ - Day arithmetic (tier-downgrade grace expiry, referral rate-limit window)
64
+ now uses `addDays` from `@classytic/primitives/calendar` — exact 24h × n
65
+ durations instead of local `setDate`, which shifted with the deploy
66
+ machine's `TZ` env. Primitives peer bumped to `>=0.9.0`.
67
+
68
+ ## [0.3.0] — 2026-06-13
69
+
70
+ Stack upgrade to mongokit 3.16 / repo-core 0.6 / primitives 0.7.2 plus
71
+ verified correctness + PACKAGE_RULES compliance fixes. No new features.
72
+
73
+ ### Fixed
74
+
75
+ - **Stack actually resolves the new floor now (CRITICAL).** The committed
76
+ lockfile silently served `@classytic/mongokit@3.13.0` +
77
+ `@classytic/repo-core@0.4.0` despite the `^3.13.3` / `^0.4.2` pins, so the
78
+ whole test suite ran against the OLD stack. Clean reinstall + peer/dev bumps
79
+ pin the resolved tree to mongokit **3.16.0**, repo-core **0.6.0**, primitives
80
+ **0.7.2** (verified with `npm ls`).
81
+ - **§P8 outbox is now session-bound on the transactional verbs.** `earnPoints`,
82
+ `adjustPoints`, `reserve`, `confirm`, `release`, and `reverseConfirmed`
83
+ previously dispatched the event AFTER `withTransaction` committed, so
84
+ `outbox.save` ran OUTSIDE the unit of work — the business doc could commit
85
+ while the durable event row was lost (or vice-versa). Dispatch now runs
86
+ INSIDE the transaction (outbox row commits atomically with the write) and the
87
+ transport publish is deferred to a post-commit flush (`runLoyaltyUnit` +
88
+ `[PENDING_EVENTS]`), so in-process subscribers never see a state that may roll
89
+ back. A new integration test asserts the save is session-bound and that a
90
+ failing `outbox.save` rolls the `$inc` balance write back.
91
+ - **Redemption slot no longer permanently blocked after release.** The
92
+ `{ ownerType, ownerId }` unique index spanned ALL statuses, so a released /
93
+ reversed reservation E11000-blocked any retry on the same order (payment
94
+ retry, cart re-attempt) forever. It is now a **partial** unique index scoped
95
+ to active statuses (`reserved`, `confirmed`) — terminal states free the slot
96
+ (PACKAGE_RULES rule 32/38/39). Re-reserve-after-release and second-active-
97
+ reserve-blocked tests added.
98
+
99
+ ### Added
100
+
101
+ - **`UnmanagedSessionError` guard (§P8).** When a host passes `ctx.session`
102
+ (it owns the outer transaction) without attaching a `[PENDING_EVENTS]` queue,
103
+ dispatch throws BEFORE any side effect instead of publishing / saving outside
104
+ the host's unit of work (which would leak ghost events on rollback).
105
+ `PENDING_EVENTS` and `flushPendingLoyaltyEvents` are exported for hosts that
106
+ manage their own transaction.
107
+ - **Capability boot gate.** `createLoyaltyEngine()` asserts the backend declares
108
+ the `transactions` capability (repo-core 0.6 / mongokit 3.16+) at engine
109
+ creation — fail loud at boot rather than with a cryptic driver error on the
110
+ first transactional call. `allowNonTransactional: true` opts out for local-dev
111
+ on standalone Mongo. New `LoyaltyCapabilityError`.
112
+ - **Cooperative cancellation in cron loops.** `processExpirations`, `awardBatch`,
113
+ `cleanupExpired` (redemption + referral), `evaluateAll`, and
114
+ `sweepExpiredOverrides` check `ctx.signal` via `throwIfAborted` between items —
115
+ abortable batches; each per-item write is its own committed unit of work, so
116
+ aborting never rolls back already-processed rows. `LoyaltyContext` gained
117
+ `signal?: AbortSignal`.
118
+
119
+ ### Changed
120
+
121
+ - **Domain errors implement `HttpError`** from `@classytic/repo-core/errors`
122
+ (was a bespoke `httpStatus` shape). Clean break: `httpStatus` → `status`,
123
+ and codes are now hierarchical + lowercase (`loyalty.<entity>.<problem>`,
124
+ e.g. `loyalty.member.not_found`, `loyalty.points.insufficient`) so arc
125
+ serializes them to `ErrorContract` correctly. No deprecation alias.
126
+ - **Peer-dep ranges raised** — `@classytic/mongokit >=3.16.0`,
127
+ `@classytic/repo-core >=0.6.0` (`>=` style). devDeps bumped to match and
128
+ primitives devDep pinned to `^0.7.2`.
129
+ - **`exactOptionalPropertyTypes: true`** enabled in tsconfig (mandatory per
130
+ PACKAGE_RULES Build Hygiene / P10); fixed the three pre-existing optional-vs-
131
+ undefined violations it surfaced.
132
+ - **tsconfig no longer emits sourcemaps or declaration maps**
133
+ (`sourceMap: false`, `declarationMap: false`) — both leaked source. tsdown
134
+ was already correct; tsconfig now matches.
135
+
136
+ ## [0.2.3] — 2026-05-08
137
+
138
+ ### Fixed
139
+
140
+ - **§P8 dispatch — `outbox.save` failures now propagate.** [src/events/dispatch.ts](src/events/dispatch.ts)
141
+ previously caught `outbox.save` errors and swallowed them silently. Per
142
+ PACKAGE_RULES §P8 the outbox row is the durability anchor for the
143
+ transactional-outbox pattern: if the save fails, the caller's transaction
144
+ MUST roll back so the business doc and the event row land atomically.
145
+ Swallowing the failure meant a parent doc could commit while the event
146
+ vanished, with no relay row to recover from. Fix re-throws after logging.
147
+ `events.publish` failures continue to be swallowed (correctly, per §P8 —
148
+ the host's outbox relay re-publishes from the durable row), but now log
149
+ before swallowing so failures are observable.
150
+ - **`DispatchDeps.logger` field added.** Optional. Both arms of the
151
+ dispatch helper log through it when set, surfacing failures in the host's
152
+ observability stack without coupling the package to a specific logger.
153
+
154
+ ### Changed
155
+
156
+ - **Peer-dep ranges tightened** — `@classytic/mongokit` `>=3.13.3`
157
+ (was `>=3.13.0`) and `@classytic/repo-core` `>=0.4.2` (was `>=0.4.0`).
158
+ 3.13.3 preserves `errorLabels` on `MongoServerError` so concurrent
159
+ transactional writes (the `claim()`-based redemption + tier-eval paths)
160
+ retry correctly under contention; 0.4.2 adds the typed `FindAllOptions`
161
+ shape with `limit?` for callers that need a bounded non-paginated read.
162
+
163
+ ### Internal
164
+
165
+ - Replaced 4 `arr.find()!` non-null assertions in [src/index.ts](src/index.ts)
166
+ + scenario tests with explicit narrowing — TS now proves what the runtime
167
+ shape already guaranteed.
168
+
169
+ ## [0.2.0]
170
+
171
+ Structural rewrite on top of the 0.1.0 shape. The public API is now a
172
+ smaller, sharper surface organized around seven subpath exports
173
+ (`/domain/enums`, `/models`, `/repositories`, `/events`, `/types`,
174
+ `/notifications`, plus the root engine factory). Legacy barrel + service
175
+ layers were removed; hosts wire business flows directly against the
176
+ repositories and the engine factory.
177
+
178
+ ### Changed
179
+
180
+ - **Domain restructure.** Dropped the `src/domain/ports/` + `src/services/`
181
+ layer in favor of thin repositories that expose exactly the operations
182
+ hosts need — `earn`, `redeem`, `adjust`, `expire`, referral credit,
183
+ tier recomputation. No more `earning.service` / `redemption.service` /
184
+ `tier.service` / `referral.service` / `ledger.service` / `member.service`
185
+ / `member-guard` indirections; the equivalent logic lives on the
186
+ repositories where it's naturally atomic.
187
+ - **Events subsystem simplified.** Removed `events/event-bus.ts` +
188
+ `events/event-types.ts` (both are internal indirection that the engine
189
+ didn't need). Event payloads are now colocated with the repository
190
+ that emits them.
191
+ - **Repositories updated to mongokit 3.11 + repo-core 0.2 conventions.**
192
+ Every repo extends `Repository<T>` directly, uses the tenant plugin via
193
+ `injectTenant`, and atomic state changes go through CAS guards.
194
+ - **Peer deps** unchanged in spirit, tightened in range:
195
+ - `@classytic/mongokit >= 3.11`
196
+ - `@classytic/primitives >= 0.1`
197
+ - `@classytic/repo-core >= 0.2`
198
+ - `mongoose >= 9.4`, `zod >= 4`
199
+ - **DevDeps:** `@classytic/primitives` devDep moved from `file:` to `>=0.1.0`
200
+ (now that primitives ships on npm); all `@classytic/*` dev ranges
201
+ standardized on `>=`.
202
+
203
+ ### Removed
204
+
205
+ - `src/services/*` — logic folded into repositories.
206
+ - `src/domain/ports/*` barrels and per-port files — structural types
207
+ now live with the repositories they describe.
208
+ - Event-bus indirection — repositories emit via the engine's configured
209
+ transport directly.
210
+
211
+ ### Added
212
+
213
+ - Subpath export `/notifications` — notification port + built-in
214
+ lifecycle payloads (tier up/down, points expiring, referral credit).
215
+ - Test restructure — 257 tests across unit + integration projects; the
216
+ deleted legacy suites were collapsed into focused scenario files that
217
+ exercise the new public API.
218
+
219
+ ### Peer compatibility
220
+
221
+ Anyone on 0.1.0 wiring against the old `*.service.ts` modules will need
222
+ to migrate to the repository methods. Signatures are similar — earn /
223
+ redeem / adjust remain the nouns — but the call sites move from
224
+ `services.earning.earnPoints(...)` to `repositories.pointTransaction.earn(...)`
225
+ and similar. No behavior changes in the ledger semantics.
226
+
227
+ ## [0.1.0]
228
+
229
+ Initial release. Loyalty points, tiers, earning rules, referrals, and
230
+ redemption engine for MongoDB. Tenant-scoped, event-emitting, atomic
231
+ point-ledger with CAS guards.
package/dist/index.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  import { n as DEFAULT_TENANT_FIELD, r as DEFAULT_TENANT_TYPE } from "./constants-DkQYkOK1.mjs";
2
- import { C as TenantIsolationError, E as ValidationError, S as SelfReferralError, T as UnmanagedSessionError, _ as RedemptionExpiredError, a as PointTransactionRepository, b as ReferralNotFoundError, c as flushPendingLoyaltyEvents, d as DuplicateReferralError, f as InsufficientPointsError, g as MemberNotFoundError, h as MemberInactiveError, i as RedemptionRepository, l as EarningRuleRepository, m as MemberAlreadyEnrolledError, n as TierDefinitionRepository, o as MemberRepository, p as LoyaltyCapabilityError, r as ReferralRepository, s as PENDING_EVENTS, t as createRepositories, u as CircularReferralError, v as RedemptionNotFoundError, w as TierNotFoundError, x as RuleNotFoundError, y as ReferralLimitExceededError } from "./repositories-DEwCbLCh.mjs";
2
+ import { C as TenantIsolationError, E as ValidationError, S as SelfReferralError, T as UnmanagedSessionError, _ as RedemptionExpiredError, a as PointTransactionRepository, b as ReferralNotFoundError, c as flushPendingLoyaltyEvents, d as DuplicateReferralError, f as InsufficientPointsError, g as MemberNotFoundError, h as MemberInactiveError, i as RedemptionRepository, l as EarningRuleRepository, m as MemberAlreadyEnrolledError, n as TierDefinitionRepository, o as MemberRepository, p as LoyaltyCapabilityError, r as ReferralRepository, s as PENDING_EVENTS, t as createRepositories, u as CircularReferralError, v as RedemptionNotFoundError, w as TierNotFoundError, x as RuleNotFoundError, y as ReferralLimitExceededError } from "./repositories-CjWfUlrA.mjs";
3
3
  import { E as InProcessLoyaltyBus, T as loyaltyEventDefinitions, _ as ReferralRecorded, a as MemberSuspended, b as TierDowngraded, c as PointsEarned, d as PointsRedeemed, f as RedemptionConfirmed, g as ReferralApproved, i as MemberReactivated, l as PointsExpired, m as RedemptionReserved, n as MemberDeactivated, p as RedemptionReleased, r as MemberEnrolled, s as PointsAdjusted, u as PointsExpiringSoon, v as ReferralRejected, w as TierUpgraded, x as TierGraceStarted, y as ReferralRewarded } from "./events-DN5_p6T0.mjs";
4
4
  import { n as createModels } from "./models-Ddh62lgO.mjs";
5
5
  import { n as createEvent, t as LoyaltyEvents } from "./loyalty-events-C38JA3cU.mjs";
@@ -86,9 +86,9 @@ declare function createModels(connection: Connection, tenant: ResolvedTenant, in
86
86
  _id: _$mongoose.Types.ObjectId;
87
87
  } & {
88
88
  __v: number;
89
- }, "id"> & {
89
+ }, "id"> & _$mongoose.HydratedDocumentOverrides<{
90
90
  id: string;
91
- }, _$mongoose.Schema<any, Model<any, any, any, any, any, any, any>, {}, {}, {}, {}, {
91
+ }>, _$mongoose.Schema<any, Model<any, any, any, any, any, any, any>, {}, {}, {}, {}, {
92
92
  timestamps: true;
93
93
  }, {
94
94
  programId: string;
@@ -167,9 +167,9 @@ declare function createModels(connection: Connection, tenant: ResolvedTenant, in
167
167
  _id: _$mongoose.Types.ObjectId;
168
168
  } & {
169
169
  __v: number;
170
- }, "id"> & {
170
+ }, "id"> & _$mongoose.HydratedDocumentOverrides<{
171
171
  id: string;
172
- }, unknown, {
172
+ }>, unknown, {
173
173
  programId: string;
174
174
  externalId: string;
175
175
  externalType: string;
@@ -298,9 +298,9 @@ declare function createModels(connection: Connection, tenant: ResolvedTenant, in
298
298
  _id: _$mongoose.Types.ObjectId;
299
299
  } & {
300
300
  __v: number;
301
- }, "id"> & {
301
+ }, "id"> & _$mongoose.HydratedDocumentOverrides<{
302
302
  id: string;
303
- }, _$mongoose.Schema<any, Model<any, any, any, any, any, any, any>, {}, {}, {}, {}, {
303
+ }>, _$mongoose.Schema<any, Model<any, any, any, any, any, any, any>, {}, {}, {}, {}, {
304
304
  timestamps: {
305
305
  createdAt: true;
306
306
  updatedAt: false;
@@ -373,9 +373,9 @@ declare function createModels(connection: Connection, tenant: ResolvedTenant, in
373
373
  _id: _$mongoose.Types.ObjectId;
374
374
  } & {
375
375
  __v: number;
376
- }, "id"> & {
376
+ }, "id"> & _$mongoose.HydratedDocumentOverrides<{
377
377
  id: string;
378
- }, unknown, {
378
+ }>, unknown, {
379
379
  programId: string;
380
380
  type: "earn" | "redeem" | "adjust" | "expire" | "transfer" | "referral_bonus";
381
381
  description: string;
@@ -519,9 +519,9 @@ declare function createModels(connection: Connection, tenant: ResolvedTenant, in
519
519
  _id: _$mongoose.Types.ObjectId;
520
520
  } & {
521
521
  __v: number;
522
- }, "id"> & {
522
+ }, "id"> & _$mongoose.HydratedDocumentOverrides<{
523
523
  id: string;
524
- }, _$mongoose.Schema<any, Model<any, any, any, any, any, any, any>, {}, {}, {}, {}, {
524
+ }>, _$mongoose.Schema<any, Model<any, any, any, any, any, any, any>, {}, {}, {}, {}, {
525
525
  timestamps: true;
526
526
  }, {
527
527
  programId: string;
@@ -618,9 +618,9 @@ declare function createModels(connection: Connection, tenant: ResolvedTenant, in
618
618
  _id: _$mongoose.Types.ObjectId;
619
619
  } & {
620
620
  __v: number;
621
- }, "id"> & {
621
+ }, "id"> & _$mongoose.HydratedDocumentOverrides<{
622
622
  id: string;
623
- }, unknown, {
623
+ }>, unknown, {
624
624
  programId: string;
625
625
  type: "order" | "action" | "category" | "tier_bonus";
626
626
  name: string;
@@ -764,9 +764,9 @@ declare function createModels(connection: Connection, tenant: ResolvedTenant, in
764
764
  _id: _$mongoose.Types.ObjectId;
765
765
  } & {
766
766
  __v: number;
767
- }, "id"> & {
767
+ }, "id"> & _$mongoose.HydratedDocumentOverrides<{
768
768
  id: string;
769
- }, _$mongoose.Schema<any, Model<any, any, any, any, any, any, any>, {}, {}, {}, {}, {
769
+ }>, _$mongoose.Schema<any, Model<any, any, any, any, any, any, any>, {}, {}, {}, {}, {
770
770
  timestamps: true;
771
771
  }, {
772
772
  programId: string;
@@ -839,9 +839,9 @@ declare function createModels(connection: Connection, tenant: ResolvedTenant, in
839
839
  _id: _$mongoose.Types.ObjectId;
840
840
  } & {
841
841
  __v: number;
842
- }, "id"> & {
842
+ }, "id"> & _$mongoose.HydratedDocumentOverrides<{
843
843
  id: string;
844
- }, unknown, {
844
+ }>, unknown, {
845
845
  programId: string;
846
846
  name: string;
847
847
  rank: number;
@@ -972,9 +972,9 @@ declare function createModels(connection: Connection, tenant: ResolvedTenant, in
972
972
  _id: _$mongoose.Types.ObjectId;
973
973
  } & {
974
974
  __v: number;
975
- }, "id"> & {
975
+ }, "id"> & _$mongoose.HydratedDocumentOverrides<{
976
976
  id: string;
977
- }, _$mongoose.Schema<any, Model<any, any, any, any, any, any, any>, {}, {}, {}, {}, {
977
+ }>, _$mongoose.Schema<any, Model<any, any, any, any, any, any, any>, {}, {}, {}, {}, {
978
978
  timestamps: true;
979
979
  }, {
980
980
  programId: string;
@@ -1050,9 +1050,9 @@ declare function createModels(connection: Connection, tenant: ResolvedTenant, in
1050
1050
  _id: _$mongoose.Types.ObjectId;
1051
1051
  } & {
1052
1052
  __v: number;
1053
- }, "id"> & {
1053
+ }, "id"> & _$mongoose.HydratedDocumentOverrides<{
1054
1054
  id: string;
1055
- }, unknown, {
1055
+ }>, unknown, {
1056
1056
  programId: string;
1057
1057
  status: "expired" | "reserved" | "confirmed" | "released" | "reversed";
1058
1058
  memberId: _$mongoose.Types.ObjectId;
@@ -1164,9 +1164,9 @@ declare function createModels(connection: Connection, tenant: ResolvedTenant, in
1164
1164
  _id: _$mongoose.Types.ObjectId;
1165
1165
  } & {
1166
1166
  __v: number;
1167
- }, "id"> & {
1167
+ }, "id"> & _$mongoose.HydratedDocumentOverrides<{
1168
1168
  id: string;
1169
- }, _$mongoose.Schema<any, Model<any, any, any, any, any, any, any>, {}, {}, {}, {}, {
1169
+ }>, _$mongoose.Schema<any, Model<any, any, any, any, any, any, any>, {}, {}, {}, {}, {
1170
1170
  timestamps: true;
1171
1171
  }, {
1172
1172
  programId: string;
@@ -1221,9 +1221,9 @@ declare function createModels(connection: Connection, tenant: ResolvedTenant, in
1221
1221
  _id: _$mongoose.Types.ObjectId;
1222
1222
  } & {
1223
1223
  __v: number;
1224
- }, "id"> & {
1224
+ }, "id"> & _$mongoose.HydratedDocumentOverrides<{
1225
1225
  id: string;
1226
- }, unknown, {
1226
+ }>, unknown, {
1227
1227
  programId: string;
1228
1228
  status: "expired" | "pending" | "approved" | "rejected";
1229
1229
  referralCode: string;
@@ -1,2 +1,2 @@
1
- import { a as PointTransactionRepository, i as RedemptionRepository, l as EarningRuleRepository, n as TierDefinitionRepository, o as MemberRepository, r as ReferralRepository, t as createRepositories } from "../repositories-DEwCbLCh.mjs";
1
+ import { a as PointTransactionRepository, i as RedemptionRepository, l as EarningRuleRepository, n as TierDefinitionRepository, o as MemberRepository, r as ReferralRepository, t as createRepositories } from "../repositories-CjWfUlrA.mjs";
2
2
  export { EarningRuleRepository, MemberRepository, PointTransactionRepository, RedemptionRepository, ReferralRepository, TierDefinitionRepository, createRepositories };
@@ -1,5 +1,5 @@
1
1
  import { n as createEvent, t as LoyaltyEvents } from "./loyalty-events-C38JA3cU.mjs";
2
- import { Repository, multiTenantPlugin } from "@classytic/mongokit";
2
+ import { Repository, appendOnlyPlugin, multiTenantPlugin } from "@classytic/mongokit";
3
3
  import { localTimeParts } from "@classytic/primitives/timezone";
4
4
  import { addDays, isoWeekday } from "@classytic/primitives/calendar";
5
5
  import { randomBytes } from "node:crypto";
@@ -709,7 +709,7 @@ var PointTransactionRepository = class extends Repository {
709
709
  unitOfWork;
710
710
  config;
711
711
  constructor(model, plugins = []) {
712
- super(model, plugins);
712
+ super(model, [...plugins, appendOnlyPlugin()]);
713
713
  }
714
714
  /** Session-bound outbox save + transport publish (PACKAGE_RULES §5.5 + P8). */
715
715
  async _dispatch(event, ctx) {
@@ -1001,7 +1001,8 @@ var PointTransactionRepository = class extends Repository {
1001
1001
  });
1002
1002
  await this.update(tx._id, { expiredAt: now }, {
1003
1003
  session,
1004
- ...tc
1004
+ ...tc,
1005
+ bypassAppendOnly: true
1005
1006
  });
1006
1007
  await this.memberRepo.updateBalance(tx.memberId, {
1007
1008
  current: -tx.points,
package/package.json CHANGED
@@ -1,116 +1,116 @@
1
- {
2
- "name": "@classytic/loyalty",
3
- "version": "0.4.0",
4
- "description": "Loyalty points, tiers, earning rules, referrals, and redemption engine for MongoDB",
5
- "author": "Classytic",
6
- "homepage": "https://www.npmjs.com/package/@classytic/loyalty",
7
- "bugs": {
8
- "url": "https://github.com/classytic/loyalty/issues"
9
- },
10
- "repository": {
11
- "type": "git",
12
- "url": "git+https://github.com/classytic/loyalty.git"
13
- },
14
- "publishConfig": {
15
- "access": "public"
16
- },
17
- "type": "module",
18
- "sideEffects": false,
19
- "engines": {
20
- "node": ">=22"
21
- },
22
- "exports": {
23
- ".": {
24
- "types": "./dist/index.d.mts",
25
- "default": "./dist/index.mjs"
26
- },
27
- "./domain/enums": {
28
- "types": "./dist/domain/enums/index.d.mts",
29
- "default": "./dist/domain/enums/index.mjs"
30
- },
31
- "./models": {
32
- "types": "./dist/models/index.d.mts",
33
- "default": "./dist/models/index.mjs"
34
- },
35
- "./repositories": {
36
- "types": "./dist/repositories/index.d.mts",
37
- "default": "./dist/repositories/index.mjs"
38
- },
39
- "./events": {
40
- "types": "./dist/events/index.d.mts",
41
- "default": "./dist/events/index.mjs"
42
- },
43
- "./types": {
44
- "types": "./dist/types/index.d.mts",
45
- "default": "./dist/types/index.mjs"
46
- },
47
- "./notifications": {
48
- "types": "./dist/notifications/index.d.mts",
49
- "default": "./dist/notifications/index.mjs"
50
- }
51
- },
52
- "main": "./dist/index.mjs",
53
- "types": "./dist/index.d.mts",
54
- "files": [
55
- "dist",
56
- "README.md",
57
- "CHANGELOG.md",
58
- "LICENSE"
59
- ],
60
- "scripts": {
61
- "build": "tsdown",
62
- "dev": "tsdown --watch",
63
- "test": "vitest run --project unit --project integration",
64
- "test:unit": "vitest run --project unit",
65
- "test:integration": "vitest run --project integration",
66
- "test:scenarios": "vitest run --project scenarios",
67
- "test:all": "vitest run",
68
- "test:watch": "vitest --project unit --project integration",
69
- "test:coverage": "vitest run --coverage",
70
- "typecheck": "tsc --noEmit",
71
- "lint": "biome check src/ tests/",
72
- "lint:fix": "biome check --write src/ tests/",
73
- "format": "biome format --write src/ tests/",
74
- "check": "biome check --write --unsafe src/ tests/",
75
- "prepublishOnly": "npm run lint && npm run typecheck && npm run test && npm run build",
76
- "push": "classytic-push",
77
- "release:tag": "node -e \"require('child_process').execSync('npm run push -- v'+require('./package.json').version,{stdio:'inherit'})\"",
78
- "release": "npm run push -- main && npm run release:tag && npm publish"
79
- },
80
- "peerDependencies": {
81
- "@classytic/mongokit": ">=3.18.0",
82
- "@classytic/primitives": ">=0.9.0",
83
- "@classytic/repo-core": ">=0.7.0",
84
- "mongoose": ">=9.4.1",
85
- "zod": ">=4.0.0"
86
- },
87
- "devDependencies": {
88
- "@biomejs/biome": "^2.4.9",
89
- "@classytic/dev-tools": "^0.2.0",
90
- "@classytic/mongokit": "^3.18.0",
91
- "@classytic/primitives": "^0.9.0",
92
- "@classytic/repo-core": "^0.7.0",
93
- "@types/node": "^25.5.0",
94
- "@vitest/coverage-v8": "^3.2.4",
95
- "knip": "^6.3.0",
96
- "mongodb-memory-server": "^10.2.3",
97
- "mongoose": "^9.4.1",
98
- "tsdown": "^0.21.5",
99
- "typescript": "^5.7.0",
100
- "vitest": "^3.0.0",
101
- "zod": "^4.3.6"
102
- },
103
- "keywords": [
104
- "loyalty",
105
- "points",
106
- "rewards",
107
- "tiers",
108
- "referral",
109
- "membership",
110
- "earning-rules",
111
- "redemption",
112
- "mongodb",
113
- "mongoose"
114
- ],
115
- "license": "MIT"
116
- }
1
+ {
2
+ "name": "@classytic/loyalty",
3
+ "version": "0.5.0",
4
+ "description": "Loyalty points, tiers, earning rules, referrals, and redemption engine for MongoDB",
5
+ "author": "Classytic",
6
+ "homepage": "https://www.npmjs.com/package/@classytic/loyalty",
7
+ "bugs": {
8
+ "url": "https://github.com/classytic/loyalty/issues"
9
+ },
10
+ "repository": {
11
+ "type": "git",
12
+ "url": "git+https://github.com/classytic/loyalty.git"
13
+ },
14
+ "publishConfig": {
15
+ "access": "public"
16
+ },
17
+ "type": "module",
18
+ "sideEffects": false,
19
+ "engines": {
20
+ "node": ">=22"
21
+ },
22
+ "exports": {
23
+ ".": {
24
+ "types": "./dist/index.d.mts",
25
+ "default": "./dist/index.mjs"
26
+ },
27
+ "./domain/enums": {
28
+ "types": "./dist/domain/enums/index.d.mts",
29
+ "default": "./dist/domain/enums/index.mjs"
30
+ },
31
+ "./models": {
32
+ "types": "./dist/models/index.d.mts",
33
+ "default": "./dist/models/index.mjs"
34
+ },
35
+ "./repositories": {
36
+ "types": "./dist/repositories/index.d.mts",
37
+ "default": "./dist/repositories/index.mjs"
38
+ },
39
+ "./events": {
40
+ "types": "./dist/events/index.d.mts",
41
+ "default": "./dist/events/index.mjs"
42
+ },
43
+ "./types": {
44
+ "types": "./dist/types/index.d.mts",
45
+ "default": "./dist/types/index.mjs"
46
+ },
47
+ "./notifications": {
48
+ "types": "./dist/notifications/index.d.mts",
49
+ "default": "./dist/notifications/index.mjs"
50
+ }
51
+ },
52
+ "main": "./dist/index.mjs",
53
+ "types": "./dist/index.d.mts",
54
+ "files": [
55
+ "dist",
56
+ "README.md",
57
+ "CHANGELOG.md",
58
+ "LICENSE"
59
+ ],
60
+ "scripts": {
61
+ "build": "tsdown",
62
+ "dev": "tsdown --watch",
63
+ "test": "vitest run --project unit --project integration",
64
+ "test:unit": "vitest run --project unit",
65
+ "test:integration": "vitest run --project integration",
66
+ "test:scenarios": "vitest run --project scenarios",
67
+ "test:all": "vitest run",
68
+ "test:watch": "vitest --project unit --project integration",
69
+ "test:coverage": "vitest run --coverage",
70
+ "typecheck": "tsc --noEmit",
71
+ "lint": "biome check src/ tests/",
72
+ "lint:fix": "biome check --write src/ tests/",
73
+ "format": "biome format --write src/ tests/",
74
+ "check": "biome check --write --unsafe src/ tests/",
75
+ "prepublishOnly": "npm run lint && npm run typecheck && npm run test && npm run build",
76
+ "push": "classytic-push",
77
+ "release:tag": "node -e \"require('child_process').execSync('npm run push -- v'+require('./package.json').version,{stdio:'inherit'})\"",
78
+ "release": "npm run push -- main && npm run release:tag && npm publish"
79
+ },
80
+ "peerDependencies": {
81
+ "@classytic/mongokit": ">=3.22.0",
82
+ "@classytic/primitives": ">=0.11.0",
83
+ "@classytic/repo-core": ">=0.13.0",
84
+ "mongoose": ">=9.4.1",
85
+ "zod": ">=4.0.0"
86
+ },
87
+ "devDependencies": {
88
+ "@biomejs/biome": "^2.4.9",
89
+ "@classytic/dev-tools": "^0.2.0",
90
+ "@classytic/mongokit": "^3.22.0",
91
+ "@classytic/primitives": "^0.11.0",
92
+ "@classytic/repo-core": "^0.13.0",
93
+ "@types/node": "^25.5.0",
94
+ "@vitest/coverage-v8": "^3.2.4",
95
+ "knip": "^6.3.0",
96
+ "mongodb-memory-server": "^10.2.3",
97
+ "mongoose": "^9.4.1",
98
+ "tsdown": "^0.21.5",
99
+ "typescript": "^5.7.0",
100
+ "vitest": "^3.0.0",
101
+ "zod": "^4.3.6"
102
+ },
103
+ "keywords": [
104
+ "loyalty",
105
+ "points",
106
+ "rewards",
107
+ "tiers",
108
+ "referral",
109
+ "membership",
110
+ "earning-rules",
111
+ "redemption",
112
+ "mongodb",
113
+ "mongoose"
114
+ ],
115
+ "license": "MIT"
116
+ }