@classytic/loyalty 0.6.0 → 0.7.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
@@ -3,6 +3,52 @@
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.7.0
7
+
8
+ ### Fixed — a `UnitOfWork` yielding a non-transactional session ran the body anyway
9
+
10
+ `runLoyaltyUnit` asserted `inTransaction()` on the HOST-session branch and not on
11
+ the `unitOfWork.withTransaction` branch, so only one of the two ways a body gets
12
+ a session was guarded. The comment above that check already stated the rule —
13
+ "guard before the body runs, not at the first emit: a verb can mutate before it
14
+ dispatches" — and it was applied to one branch.
15
+
16
+ It matters because the balance verbs compensate by THROWING to roll an `$inc`
17
+ back. Without a real transaction that buys nothing: `redeem` decrements a
18
+ member's points, sees the balance go negative, throws — and the points stay
19
+ gone while the caller is told the redemption failed. A lying `withTransaction`
20
+ was previously caught at the first event DISPATCH, by which time the write had
21
+ landed.
22
+
23
+ 3 cases in `tests/unit/unit-of-work-transaction-guard.test.ts`, asserting the
24
+ body did NOT run — the rejection alone would pass even if it had mutated first.
25
+ Falsified: removing the guard fails 2 of 3.
26
+
27
+ ### Fixed — the transaction guard must not override `allowNonTransactional`
28
+
29
+ The fix above asserted `inTransaction()` unconditionally, which also made it
30
+ wrong: `allowNonTransactional` is an explicit host opt-in (loyalty already
31
+ warns about it at boot) for a standalone-Mongo deployment where
32
+ `withTransaction({ allowFallback })` runs the callback with no transaction ON
33
+ PURPOSE. The guard turned that documented degradation into a hard failure, so
34
+ a standalone-Mongo host lost point earn / redemption / reversal entirely — a
35
+ general rule silently defeating a specific decision, the inverse of what the
36
+ guard exists to catch.
37
+
38
+ `UnitOfWork` now states whether it may degrade, and the consent travels on the
39
+ internal context because the same guard also runs from `dispatchLoyaltyEvent`,
40
+ which never sees the unit of work. Absent still means no: an implementation
41
+ that yields a bare session without saying so is still refused, and a
42
+ host-supplied `ctx.session` has no opt-in and always asserts. Falsified both
43
+ ways in `unit-of-work-transaction-guard.test.ts`.
44
+
45
+ ### Changed — peer floors raised to current
46
+
47
+ - `@classytic/mongokit` `>=3.33.0` → **`>=3.38.1`**
48
+ - `@classytic/primitives` `>=0.23.0` → **`>=0.27.0`**
49
+ - `@classytic/repo-core` `>=0.24.0` → **`>=0.27.1`**
50
+ - `@classytic/validation` `>=0.2.0` → **`>=0.5.0`**
51
+
6
52
  ## [0.6.0] — 2026-08-17
7
53
 
8
54
  ### Added
@@ -2,7 +2,7 @@ import { i as ReferralStatus, r as RedemptionStatus } from "./index-D_Oe3aDk.mjs
2
2
  import { S as EarningRule, _ as TierEvaluationBatch, d as EarningResult, f as ExpirationResult, g as TierEvaluation, h as RedemptionValidation, o as LoyaltyContext, v as PointTransaction, x as TierDefinition, y as Member } from "./index-Cp_sXNpG.mjs";
3
3
  import { t as NotificationAdapter } from "./notification.port-wY4IKimz.mjs";
4
4
  import { c as ResolvedConfig, s as RepositoryPlugins } from "./config-CtkGj3xZ.mjs";
5
- import { n as LoyaltyModels } from "./index-D6GvuvmF.mjs";
5
+ import { n as LoyaltyModels } from "./index-BIhVuwIZ.mjs";
6
6
  import { PluginType, Repository } from "@classytic/mongokit";
7
7
  import { ResolvedTenantConfig } from "@classytic/repo-core/tenant";
8
8
  import { ClientSession, Model } from "mongoose";
@@ -12,6 +12,17 @@ import { ApprovalChain } from "@classytic/primitives/approval";
12
12
  //#region src/domain/ports/unit-of-work.port.d.ts
13
13
  interface UnitOfWork {
14
14
  withTransaction<T>(callback: (session: ClientSession) => Promise<T>): Promise<T>;
15
+ /**
16
+ * This unit of work MAY hand the callback a session with no live transaction.
17
+ *
18
+ * True only where the host opted into `allowNonTransactional` — a standalone
19
+ * mongod accepts the degradation knowingly, and loyalty logs it at boot. The
20
+ * guard that otherwise refuses a transaction-less session reads this, so an
21
+ * accepted degradation is not re-refused one layer down. Absent means NO:
22
+ * an implementation that yields a bare session without saying so is a defect,
23
+ * and stays one.
24
+ */
25
+ readonly allowsNonTransactional?: boolean;
15
26
  }
16
27
  //#endregion
17
28
  //#region src/repositories/member.repository.d.ts
@@ -11,6 +11,7 @@ declare function createEarningRuleSchema(): mongoose.Schema<any, mongoose.Model<
11
11
  name: string;
12
12
  status: "active" | "paused" | "expired";
13
13
  priority: number;
14
+ metadata?: any;
14
15
  description?: string | null;
15
16
  conditions?: {
16
17
  categories: string[];
@@ -34,13 +35,13 @@ declare function createEarningRuleSchema(): mongoose.Schema<any, mongoose.Model<
34
35
  } | null;
35
36
  startsAt?: NativeDate | null;
36
37
  endsAt?: NativeDate | null;
37
- metadata?: any;
38
38
  } & mongoose.DefaultTimestampProps, mongoose.Document<unknown, {}, {
39
39
  programId: string;
40
40
  type: "order" | "action" | "category" | "tier_bonus";
41
41
  name: string;
42
42
  status: "active" | "paused" | "expired";
43
43
  priority: number;
44
+ metadata?: any;
44
45
  description?: string | null;
45
46
  conditions?: {
46
47
  categories: string[];
@@ -64,7 +65,6 @@ declare function createEarningRuleSchema(): mongoose.Schema<any, mongoose.Model<
64
65
  } | null;
65
66
  startsAt?: NativeDate | null;
66
67
  endsAt?: NativeDate | null;
67
- metadata?: any;
68
68
  } & mongoose.DefaultTimestampProps, {
69
69
  id: string;
70
70
  }, Omit<mongoose.DefaultSchemaOptions, "timestamps"> & {
@@ -75,6 +75,7 @@ declare function createEarningRuleSchema(): mongoose.Schema<any, mongoose.Model<
75
75
  name: string;
76
76
  status: "active" | "paused" | "expired";
77
77
  priority: number;
78
+ metadata?: any;
78
79
  description?: string | null;
79
80
  conditions?: {
80
81
  categories: string[];
@@ -98,7 +99,6 @@ declare function createEarningRuleSchema(): mongoose.Schema<any, mongoose.Model<
98
99
  } | null;
99
100
  startsAt?: NativeDate | null;
100
101
  endsAt?: NativeDate | null;
101
- metadata?: any;
102
102
  } & mongoose.DefaultTimestampProps & {
103
103
  _id: mongoose.Types.ObjectId;
104
104
  } & {
@@ -111,6 +111,7 @@ declare function createEarningRuleSchema(): mongoose.Schema<any, mongoose.Model<
111
111
  name: string;
112
112
  status: "active" | "paused" | "expired";
113
113
  priority: number;
114
+ metadata?: any;
114
115
  description?: string | null;
115
116
  conditions?: {
116
117
  categories: string[];
@@ -134,7 +135,6 @@ declare function createEarningRuleSchema(): mongoose.Schema<any, mongoose.Model<
134
135
  } | null;
135
136
  startsAt?: NativeDate | null;
136
137
  endsAt?: NativeDate | null;
137
- metadata?: any;
138
138
  createdAt: NativeDate;
139
139
  updatedAt: NativeDate;
140
140
  } & {
@@ -153,8 +153,8 @@ declare function createMemberSchema(): mongoose.Schema<any, mongoose.Model<any,
153
153
  externalType: string;
154
154
  enrolledAt: NativeDate;
155
155
  metadata?: any;
156
- cardId?: string | null;
157
156
  referralCode?: string | null;
157
+ cardId?: string | null;
158
158
  tier?: string | null;
159
159
  tierOverride?: string | null;
160
160
  tierOverrideReason?: string | null;
@@ -176,8 +176,8 @@ declare function createMemberSchema(): mongoose.Schema<any, mongoose.Model<any,
176
176
  externalType: string;
177
177
  enrolledAt: NativeDate;
178
178
  metadata?: any;
179
- cardId?: string | null;
180
179
  referralCode?: string | null;
180
+ cardId?: string | null;
181
181
  tier?: string | null;
182
182
  tierOverride?: string | null;
183
183
  tierOverrideReason?: string | null;
@@ -203,8 +203,8 @@ declare function createMemberSchema(): mongoose.Schema<any, mongoose.Model<any,
203
203
  externalType: string;
204
204
  enrolledAt: NativeDate;
205
205
  metadata?: any;
206
- cardId?: string | null;
207
206
  referralCode?: string | null;
207
+ cardId?: string | null;
208
208
  tier?: string | null;
209
209
  tierOverride?: string | null;
210
210
  tierOverrideReason?: string | null;
@@ -232,8 +232,8 @@ declare function createMemberSchema(): mongoose.Schema<any, mongoose.Model<any,
232
232
  externalType: string;
233
233
  enrolledAt: NativeDate;
234
234
  metadata?: any;
235
- cardId?: string | null;
236
235
  referralCode?: string | null;
236
+ cardId?: string | null;
237
237
  tier?: string | null;
238
238
  tierOverride?: string | null;
239
239
  tierOverrideReason?: string | null;
@@ -271,15 +271,15 @@ declare function createPointTransactionSchema(): mongoose.Schema<any, mongoose.M
271
271
  balanceAfter: number;
272
272
  actorId: string;
273
273
  metadata?: any;
274
- expiresAt?: NativeDate | null;
275
- approvals?: any;
276
- approvalPolicyId?: string | null;
277
- approvalPolicyVersion?: number | null;
278
274
  referenceType?: string | null;
279
275
  referenceId?: string | null;
280
276
  idempotencyKey?: string | null;
281
277
  earningRuleId?: mongoose.Types.ObjectId | null;
278
+ expiresAt?: NativeDate | null;
282
279
  expiredAt?: NativeDate | null;
280
+ approvals?: any;
281
+ approvalPolicyId?: string | null;
282
+ approvalPolicyVersion?: number | null;
283
283
  createdAt: NativeDate;
284
284
  }, mongoose.Document<unknown, {}, {
285
285
  programId: string;
@@ -290,15 +290,15 @@ declare function createPointTransactionSchema(): mongoose.Schema<any, mongoose.M
290
290
  balanceAfter: number;
291
291
  actorId: string;
292
292
  metadata?: any;
293
- expiresAt?: NativeDate | null;
294
- approvals?: any;
295
- approvalPolicyId?: string | null;
296
- approvalPolicyVersion?: number | null;
297
293
  referenceType?: string | null;
298
294
  referenceId?: string | null;
299
295
  idempotencyKey?: string | null;
300
296
  earningRuleId?: mongoose.Types.ObjectId | null;
297
+ expiresAt?: NativeDate | null;
301
298
  expiredAt?: NativeDate | null;
299
+ approvals?: any;
300
+ approvalPolicyId?: string | null;
301
+ approvalPolicyVersion?: number | null;
302
302
  createdAt: NativeDate;
303
303
  }, {
304
304
  id: string;
@@ -316,15 +316,15 @@ declare function createPointTransactionSchema(): mongoose.Schema<any, mongoose.M
316
316
  balanceAfter: number;
317
317
  actorId: string;
318
318
  metadata?: any;
319
- expiresAt?: NativeDate | null;
320
- approvals?: any;
321
- approvalPolicyId?: string | null;
322
- approvalPolicyVersion?: number | null;
323
319
  referenceType?: string | null;
324
320
  referenceId?: string | null;
325
321
  idempotencyKey?: string | null;
326
322
  earningRuleId?: mongoose.Types.ObjectId | null;
323
+ expiresAt?: NativeDate | null;
327
324
  expiredAt?: NativeDate | null;
325
+ approvals?: any;
326
+ approvalPolicyId?: string | null;
327
+ approvalPolicyVersion?: number | null;
328
328
  createdAt: NativeDate;
329
329
  } & {
330
330
  _id: mongoose.Types.ObjectId;
@@ -341,15 +341,15 @@ declare function createPointTransactionSchema(): mongoose.Schema<any, mongoose.M
341
341
  balanceAfter: number;
342
342
  actorId: string;
343
343
  metadata?: any;
344
- expiresAt?: NativeDate | null;
345
- approvals?: any;
346
- approvalPolicyId?: string | null;
347
- approvalPolicyVersion?: number | null;
348
344
  referenceType?: string | null;
349
345
  referenceId?: string | null;
350
346
  idempotencyKey?: string | null;
351
347
  earningRuleId?: mongoose.Types.ObjectId | null;
348
+ expiresAt?: NativeDate | null;
352
349
  expiredAt?: NativeDate | null;
350
+ approvals?: any;
351
+ approvalPolicyId?: string | null;
352
+ approvalPolicyVersion?: number | null;
353
353
  createdAt: NativeDate;
354
354
  } & {
355
355
  _id: mongoose.Types.ObjectId;
@@ -371,6 +371,9 @@ declare function createRedemptionSchema(): mongoose.Schema<any, mongoose.Model<a
371
371
  ownerType: string;
372
372
  ownerId: string;
373
373
  expiresAt?: NativeDate | null;
374
+ approvals?: any;
375
+ approvalPolicyId?: string | null;
376
+ approvalPolicyVersion?: number | null;
374
377
  confirmedAt?: NativeDate | null;
375
378
  releasedAt?: NativeDate | null;
376
379
  reversedAt?: NativeDate | null;
@@ -379,9 +382,6 @@ declare function createRedemptionSchema(): mongoose.Schema<any, mongoose.Model<a
379
382
  transactionId?: mongoose.Types.ObjectId | null;
380
383
  initialBalance?: number | null;
381
384
  remainingBalance?: number | null;
382
- approvals?: any;
383
- approvalPolicyId?: string | null;
384
- approvalPolicyVersion?: number | null;
385
385
  } & mongoose.DefaultTimestampProps, mongoose.Document<unknown, {}, {
386
386
  programId: string;
387
387
  status: "expired" | "reserved" | "confirmed" | "released" | "reversed";
@@ -393,6 +393,9 @@ declare function createRedemptionSchema(): mongoose.Schema<any, mongoose.Model<a
393
393
  ownerType: string;
394
394
  ownerId: string;
395
395
  expiresAt?: NativeDate | null;
396
+ approvals?: any;
397
+ approvalPolicyId?: string | null;
398
+ approvalPolicyVersion?: number | null;
396
399
  confirmedAt?: NativeDate | null;
397
400
  releasedAt?: NativeDate | null;
398
401
  reversedAt?: NativeDate | null;
@@ -401,9 +404,6 @@ declare function createRedemptionSchema(): mongoose.Schema<any, mongoose.Model<a
401
404
  transactionId?: mongoose.Types.ObjectId | null;
402
405
  initialBalance?: number | null;
403
406
  remainingBalance?: number | null;
404
- approvals?: any;
405
- approvalPolicyId?: string | null;
406
- approvalPolicyVersion?: number | null;
407
407
  } & mongoose.DefaultTimestampProps, {
408
408
  id: string;
409
409
  }, Omit<mongoose.DefaultSchemaOptions, "timestamps"> & {
@@ -419,6 +419,9 @@ declare function createRedemptionSchema(): mongoose.Schema<any, mongoose.Model<a
419
419
  ownerType: string;
420
420
  ownerId: string;
421
421
  expiresAt?: NativeDate | null;
422
+ approvals?: any;
423
+ approvalPolicyId?: string | null;
424
+ approvalPolicyVersion?: number | null;
422
425
  confirmedAt?: NativeDate | null;
423
426
  releasedAt?: NativeDate | null;
424
427
  reversedAt?: NativeDate | null;
@@ -427,9 +430,6 @@ declare function createRedemptionSchema(): mongoose.Schema<any, mongoose.Model<a
427
430
  transactionId?: mongoose.Types.ObjectId | null;
428
431
  initialBalance?: number | null;
429
432
  remainingBalance?: number | null;
430
- approvals?: any;
431
- approvalPolicyId?: string | null;
432
- approvalPolicyVersion?: number | null;
433
433
  } & mongoose.DefaultTimestampProps & {
434
434
  _id: mongoose.Types.ObjectId;
435
435
  } & {
@@ -447,6 +447,9 @@ declare function createRedemptionSchema(): mongoose.Schema<any, mongoose.Model<a
447
447
  ownerType: string;
448
448
  ownerId: string;
449
449
  expiresAt?: NativeDate | null;
450
+ approvals?: any;
451
+ approvalPolicyId?: string | null;
452
+ approvalPolicyVersion?: number | null;
450
453
  confirmedAt?: NativeDate | null;
451
454
  releasedAt?: NativeDate | null;
452
455
  reversedAt?: NativeDate | null;
@@ -455,9 +458,6 @@ declare function createRedemptionSchema(): mongoose.Schema<any, mongoose.Model<a
455
458
  transactionId?: mongoose.Types.ObjectId | null;
456
459
  initialBalance?: number | null;
457
460
  remainingBalance?: number | null;
458
- approvals?: any;
459
- approvalPolicyId?: string | null;
460
- approvalPolicyVersion?: number | null;
461
461
  createdAt: NativeDate;
462
462
  updatedAt: NativeDate;
463
463
  } & {
@@ -472,9 +472,9 @@ declare function createReferralSchema(): mongoose.Schema<any, mongoose.Model<any
472
472
  }, {
473
473
  programId: string;
474
474
  status: "expired" | "pending" | "approved" | "rejected";
475
- referralCode: string;
476
475
  referrerId: mongoose.Types.ObjectId;
477
476
  refereeId: mongoose.Types.ObjectId;
477
+ referralCode: string;
478
478
  metadata?: any;
479
479
  expiresAt?: NativeDate | null;
480
480
  referrerRewardPoints?: number | null;
@@ -487,9 +487,9 @@ declare function createReferralSchema(): mongoose.Schema<any, mongoose.Model<any
487
487
  } & mongoose.DefaultTimestampProps, mongoose.Document<unknown, {}, {
488
488
  programId: string;
489
489
  status: "expired" | "pending" | "approved" | "rejected";
490
- referralCode: string;
491
490
  referrerId: mongoose.Types.ObjectId;
492
491
  refereeId: mongoose.Types.ObjectId;
492
+ referralCode: string;
493
493
  metadata?: any;
494
494
  expiresAt?: NativeDate | null;
495
495
  referrerRewardPoints?: number | null;
@@ -506,9 +506,9 @@ declare function createReferralSchema(): mongoose.Schema<any, mongoose.Model<any
506
506
  }> & Omit<{
507
507
  programId: string;
508
508
  status: "expired" | "pending" | "approved" | "rejected";
509
- referralCode: string;
510
509
  referrerId: mongoose.Types.ObjectId;
511
510
  refereeId: mongoose.Types.ObjectId;
511
+ referralCode: string;
512
512
  metadata?: any;
513
513
  expiresAt?: NativeDate | null;
514
514
  referrerRewardPoints?: number | null;
@@ -527,9 +527,9 @@ declare function createReferralSchema(): mongoose.Schema<any, mongoose.Model<any
527
527
  }>, unknown, {
528
528
  programId: string;
529
529
  status: "expired" | "pending" | "approved" | "rejected";
530
- referralCode: string;
531
530
  referrerId: mongoose.Types.ObjectId;
532
531
  refereeId: mongoose.Types.ObjectId;
532
+ referralCode: string;
533
533
  metadata?: any;
534
534
  expiresAt?: NativeDate | null;
535
535
  referrerRewardPoints?: number | null;
@@ -554,7 +554,6 @@ declare function createTierDefinitionSchema(): mongoose.Schema<any, mongoose.Mod
554
554
  programId: string;
555
555
  name: string;
556
556
  rank: number;
557
- metadata?: any;
558
557
  qualificationCriteria?: {
559
558
  minLifetimePoints?: number | null;
560
559
  minLifetimeSpend?: number | null;
@@ -571,11 +570,11 @@ declare function createTierDefinitionSchema(): mongoose.Schema<any, mongoose.Mod
571
570
  gracePeriodDays: number;
572
571
  } | null;
573
572
  color?: string | null;
573
+ metadata?: any;
574
574
  } & mongoose.DefaultTimestampProps, mongoose.Document<unknown, {}, {
575
575
  programId: string;
576
576
  name: string;
577
577
  rank: number;
578
- metadata?: any;
579
578
  qualificationCriteria?: {
580
579
  minLifetimePoints?: number | null;
581
580
  minLifetimeSpend?: number | null;
@@ -592,6 +591,7 @@ declare function createTierDefinitionSchema(): mongoose.Schema<any, mongoose.Mod
592
591
  gracePeriodDays: number;
593
592
  } | null;
594
593
  color?: string | null;
594
+ metadata?: any;
595
595
  } & mongoose.DefaultTimestampProps, {
596
596
  id: string;
597
597
  }, Omit<mongoose.DefaultSchemaOptions, "timestamps"> & {
@@ -600,7 +600,6 @@ declare function createTierDefinitionSchema(): mongoose.Schema<any, mongoose.Mod
600
600
  programId: string;
601
601
  name: string;
602
602
  rank: number;
603
- metadata?: any;
604
603
  qualificationCriteria?: {
605
604
  minLifetimePoints?: number | null;
606
605
  minLifetimeSpend?: number | null;
@@ -617,6 +616,7 @@ declare function createTierDefinitionSchema(): mongoose.Schema<any, mongoose.Mod
617
616
  gracePeriodDays: number;
618
617
  } | null;
619
618
  color?: string | null;
619
+ metadata?: any;
620
620
  } & mongoose.DefaultTimestampProps & {
621
621
  _id: mongoose.Types.ObjectId;
622
622
  } & {
@@ -627,7 +627,6 @@ declare function createTierDefinitionSchema(): mongoose.Schema<any, mongoose.Mod
627
627
  programId: string;
628
628
  name: string;
629
629
  rank: number;
630
- metadata?: any;
631
630
  qualificationCriteria?: {
632
631
  minLifetimePoints?: number | null;
633
632
  minLifetimeSpend?: number | null;
@@ -644,6 +643,7 @@ declare function createTierDefinitionSchema(): mongoose.Schema<any, mongoose.Mod
644
643
  gracePeriodDays: number;
645
644
  } | null;
646
645
  color?: string | null;
646
+ metadata?: any;
647
647
  createdAt: NativeDate;
648
648
  updatedAt: NativeDate;
649
649
  } & {
package/dist/index.d.mts CHANGED
@@ -2,8 +2,8 @@ import { S as EarningRule, a as EnrollInput, b as MemberBalance, c as UpdateTier
2
2
  import { t as NotificationAdapter } from "./notification.port-wY4IKimz.mjs";
3
3
  import { c as ResolvedConfig, i as LoyaltyShape, l as ResolvedTenant, n as LoyaltyModelName, r as LoyaltyRuntime } from "./config-CtkGj3xZ.mjs";
4
4
  import { A as PointsExpired, B as ReferralRewarded, C as MemberDeactivated, E as MemberSuspended, F as RedemptionReserved, G as TierUpgraded, H as TierGraceStarted, J as InProcessLoyaltyBusOptions, K as loyaltyEventDefinitions, L as ReferralApproved, M as PointsRedeemed, N as RedemptionConfirmed, O as PointsAdjusted, P as RedemptionReleased, R as ReferralRecorded, S as LoyaltyEventSchema, T as MemberReactivated, V as TierDowngraded, Y as createEvent, _ as TierChangedPayload, a as LoyaltyEventMap, b as LoyaltyEventDefinition, c as LoyaltyEvents, d as PointsAdjustedPayload, f as PointsEarnedPayload, g as ReferralPayload, j as PointsExpiringSoon, k as PointsEarned, l as MemberLifecyclePayload, m as RedemptionPayload, o as LoyaltyEventName, p as PointsExpiredPayload, q as InProcessLoyaltyBus, s as LoyaltyEventPayload, w as MemberEnrolled, x as LoyaltyEventPayloadOf, z as ReferralRejected } from "./index-YUFamhDk.mjs";
5
- import { n as LoyaltyModels, r as defineLoyaltyModels, t as DefineLoyaltyModelsConfig } from "./index-D6GvuvmF.mjs";
6
- import { a as ReferralRepository, c as Redemption, d as MemberRepository, f as UnitOfWork, i as TierDefinitionRepository, l as EarningRuleRepository, n as LoyaltyRepositories, o as Referral, s as RedemptionRepository, u as PointTransactionRepository } from "./index-CR2k7jQ8.mjs";
5
+ import { n as LoyaltyModels, r as defineLoyaltyModels, t as DefineLoyaltyModelsConfig } from "./index-BIhVuwIZ.mjs";
6
+ import { a as ReferralRepository, c as Redemption, d as MemberRepository, f as UnitOfWork, i as TierDefinitionRepository, l as EarningRuleRepository, n as LoyaltyRepositories, o as Referral, s as RedemptionRepository, u as PointTransactionRepository } from "./index-BIPHUsfR.mjs";
7
7
  import { Connection } from "mongoose";
8
8
  import { DomainEvent, DomainEvent as DomainEvent$1, EventHandler, EventTransport, EventTransport as EventTransport$1 } from "@classytic/primitives/events";
9
9
  import { OutboxStore } from "@classytic/primitives/outbox";
package/dist/index.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  import "./constants-DOPBh6kt.mjs";
2
- import { C as SessionNotInTransactionError, D as ValidationError, E as UnmanagedSessionError, S as SelfReferralError, T as TierNotFoundError, _ 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 TenantIsolationError, x as RuleNotFoundError, y as ReferralLimitExceededError } from "./repositories-B676z4bE.mjs";
2
+ import { C as SessionNotInTransactionError, D as ValidationError, E as UnmanagedSessionError, S as SelfReferralError, T as TierNotFoundError, _ 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 TenantIsolationError, x as RuleNotFoundError, y as ReferralLimitExceededError } from "./repositories-DErtHQou.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-BLjg3uLA.mjs";
4
4
  import { t as defineLoyaltyModels } from "./models-weWqS5Bc.mjs";
5
5
  import { n as createEvent, t as LoyaltyEvents } from "./loyalty-events-DXirolM_.mjs";
@@ -49,6 +49,10 @@ var MongoUnitOfWork = class {
49
49
  this.connection = connection;
50
50
  this.allowFallback = allowFallback;
51
51
  }
52
+ /** The fallback IS the transaction-less path — say so, don't make callers infer it. */
53
+ get allowsNonTransactional() {
54
+ return this.allowFallback;
55
+ }
52
56
  async withTransaction(cb) {
53
57
  return withTransaction(this.connection, cb, this.allowFallback ? { allowFallback: true } : {});
54
58
  }
@@ -1,3 +1,3 @@
1
1
  import { n as LoyaltyModelName } from "../config-CtkGj3xZ.mjs";
2
- import { n as LoyaltyModels, r as defineLoyaltyModels, t as DefineLoyaltyModelsConfig } from "../index-D6GvuvmF.mjs";
2
+ import { n as LoyaltyModels, r as defineLoyaltyModels, t as DefineLoyaltyModelsConfig } from "../index-BIhVuwIZ.mjs";
3
3
  export { DefineLoyaltyModelsConfig, type LoyaltyModelName, LoyaltyModels, defineLoyaltyModels };
@@ -1,2 +1,2 @@
1
- import { a as ReferralRepository, d as MemberRepository, i as TierDefinitionRepository, l as EarningRuleRepository, n as LoyaltyRepositories, r as createRepositories, s as RedemptionRepository, t as CreateRepositoriesInput, u as PointTransactionRepository } from "../index-CR2k7jQ8.mjs";
1
+ import { a as ReferralRepository, d as MemberRepository, i as TierDefinitionRepository, l as EarningRuleRepository, n as LoyaltyRepositories, r as createRepositories, s as RedemptionRepository, t as CreateRepositoriesInput, u as PointTransactionRepository } from "../index-BIPHUsfR.mjs";
2
2
  export { CreateRepositoriesInput, EarningRuleRepository, LoyaltyRepositories, MemberRepository, PointTransactionRepository, RedemptionRepository, ReferralRepository, TierDefinitionRepository, createRepositories };
@@ -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-B676z4bE.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-DErtHQou.mjs";
2
2
  export { EarningRuleRepository, MemberRepository, PointTransactionRepository, RedemptionRepository, ReferralRepository, TierDefinitionRepository, createRepositories };
@@ -485,6 +485,16 @@ var EarningRuleRepository = class extends Repository {
485
485
  */
486
486
  const PENDING_EVENTS = Symbol.for("@classytic/loyalty:pendingEvents");
487
487
  /**
488
+ * Set when the unit of work was PERMITTED to run without a transaction
489
+ * (`allowNonTransactional`), so the session-in-transaction guard knows the
490
+ * degradation was chosen rather than stumbled into.
491
+ *
492
+ * Carried on the context because the guard also runs from
493
+ * `dispatchLoyaltyEvent`, which never sees the unit of work. Internal and
494
+ * `Symbol.for`, so no host can set it by accident or on purpose.
495
+ */
496
+ const NON_TRANSACTIONAL_CONSENTED = Symbol.for("@classytic/loyalty:nonTransactionalConsented");
497
+ /**
488
498
  * A caller-supplied `ctx.session` is only evidence of a unit of work if it
489
499
  * has an ACTIVE transaction. A bare `startSession()` commits every write
490
500
  * independently, so treating it as atomic is how a balance `$inc` lands with
@@ -497,6 +507,7 @@ const PENDING_EVENTS = Symbol.for("@classytic/loyalty:pendingEvents");
497
507
  */
498
508
  function assertSessionInTransaction(ctx) {
499
509
  if (!ctx.session) return;
510
+ if (ctx[NON_TRANSACTIONAL_CONSENTED]) return;
500
511
  const session = ctx.session;
501
512
  if (typeof session.inTransaction !== "function" || !session.inTransaction()) throw new SessionNotInTransactionError();
502
513
  }
@@ -549,11 +560,14 @@ async function runLoyaltyUnit(unitOfWork, deps, ctx, body) {
549
560
  const pending = [];
550
561
  const result = await unitOfWork.withTransaction(async (session) => {
551
562
  pending.length = 0;
552
- return body({
563
+ const innerCtx = {
553
564
  ...internalCtx,
554
565
  session,
555
- [PENDING_EVENTS]: pending
556
- });
566
+ [PENDING_EVENTS]: pending,
567
+ ...unitOfWork.allowsNonTransactional ? { [NON_TRANSACTIONAL_CONSENTED]: true } : {}
568
+ };
569
+ assertSessionInTransaction(innerCtx);
570
+ return body(innerCtx);
557
571
  });
558
572
  await flushPendingLoyaltyEvents(deps.events, pending, deps.logger);
559
573
  return result;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@classytic/loyalty",
3
- "version": "0.6.0",
3
+ "version": "0.7.0",
4
4
  "description": "Loyalty points, tiers, earning rules, referrals, and redemption engine for MongoDB",
5
5
  "author": "Classytic",
6
6
  "homepage": "https://www.npmjs.com/package/@classytic/loyalty",
@@ -78,20 +78,20 @@
78
78
  "release": "npm run push -- main && npm run release:tag && npm publish"
79
79
  },
80
80
  "peerDependencies": {
81
- "@classytic/mongokit": ">=3.33.0",
82
- "@classytic/primitives": ">=0.23.0",
83
- "@classytic/repo-core": ">=0.24.0",
84
- "@classytic/validation": ">=0.2.0",
81
+ "@classytic/mongokit": ">=3.38.1",
82
+ "@classytic/primitives": ">=0.27.0",
83
+ "@classytic/repo-core": ">=0.27.1",
84
+ "@classytic/validation": ">=0.5.0",
85
85
  "mongoose": ">=9.4.1",
86
86
  "zod": ">=4.0.0"
87
87
  },
88
88
  "devDependencies": {
89
89
  "@biomejs/biome": "^2.4.9",
90
90
  "@classytic/dev-tools": "^0.2.0",
91
- "@classytic/mongokit": "^3.33.0",
92
- "@classytic/primitives": ">=0.23.0",
93
- "@classytic/repo-core": ">=0.24.0",
94
- "@classytic/validation": ">=0.4.0",
91
+ "@classytic/mongokit": "^3.38.1",
92
+ "@classytic/primitives": ">=0.27.0",
93
+ "@classytic/repo-core": ">=0.27.1",
94
+ "@classytic/validation": ">=0.5.0",
95
95
  "@types/node": "^25.5.0",
96
96
  "@vitest/coverage-v8": "^3.2.4",
97
97
  "knip": "^6.3.0",