@classytic/loyalty 0.6.0 → 0.10.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.
Files changed (32) hide show
  1. package/CHANGELOG.md +180 -0
  2. package/dist/constants-DfrENxFG.mjs +75 -0
  3. package/dist/domain/earn-base.d.mts +35 -0
  4. package/dist/domain/earn-base.mjs +37 -0
  5. package/dist/domain/enums/index.d.mts +2 -2
  6. package/dist/domain/enums/index.mjs +1 -2
  7. package/dist/domain/rule-schedule.d.mts +24 -0
  8. package/dist/domain/rule-schedule.mjs +12 -0
  9. package/dist/events/index.d.mts +2 -2
  10. package/dist/events/index.mjs +3 -3
  11. package/dist/{events-BLjg3uLA.mjs → events-DhZolW61.mjs} +104 -4
  12. package/dist/{index-CR2k7jQ8.d.mts → index-BvyzKU7R.d.mts} +294 -34
  13. package/dist/{index-D6GvuvmF.d.mts → index-CorB0JL6.d.mts} +259 -113
  14. package/dist/index-DRrEhE0r.d.mts +604 -0
  15. package/dist/{index-YUFamhDk.d.mts → index-Db5Gb7x_.d.mts} +187 -39
  16. package/dist/index-tjMMmPzU.d.mts +36 -0
  17. package/dist/index.d.mts +121 -30
  18. package/dist/index.mjs +63 -10
  19. package/dist/{loyalty-events-DXirolM_.mjs → loyalty-events-j29B66YW.mjs} +20 -13
  20. package/dist/models/index.d.mts +2 -2
  21. package/dist/models/index.mjs +1 -2
  22. package/dist/{models-weWqS5Bc.mjs → models-BEGLduz1.mjs} +276 -7
  23. package/dist/notifications/index.d.mts +3 -4
  24. package/dist/repositories/index.d.mts +2 -2
  25. package/dist/repositories/index.mjs +1 -2
  26. package/dist/{repositories-B676z4bE.mjs → repositories-ER68JmUo.mjs} +1178 -102
  27. package/dist/types/index.d.mts +2 -3
  28. package/package.json +39 -22
  29. package/dist/config-CtkGj3xZ.d.mts +0 -178
  30. package/dist/constants-DOPBh6kt.mjs +0 -48
  31. package/dist/index-Cp_sXNpG.d.mts +0 -349
  32. package/dist/index-D_Oe3aDk.d.mts +0 -19
@@ -1,8 +1,7 @@
1
- import { i as ReferralStatus, r as RedemptionStatus } from "./index-D_Oe3aDk.mjs";
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";
1
+ import { a as ReferralStatus, i as RedemptionStatus, t as EarnSource } from "./index-tjMMmPzU.mjs";
2
+ import { A as RepositoryPlugins, I as TierDefinition, L as EarningRule, P as StampRewardIssuer, _ as RedemptionValidation, b as PointTransaction, f as EarningResult, h as LoyaltyLiabilitySnapshot, j as ResolvedConfig, o as LoyaltyContext, p as ExpirationResult, u as EarnReversalResult, v as TierEvaluation, x as Member, y as TierEvaluationBatch } from "./index-DRrEhE0r.mjs";
3
3
  import { t as NotificationAdapter } from "./notification.port-wY4IKimz.mjs";
4
- import { c as ResolvedConfig, s as RepositoryPlugins } from "./config-CtkGj3xZ.mjs";
5
- import { n as LoyaltyModels } from "./index-D6GvuvmF.mjs";
4
+ import { c as StampOrderInput, d as StampProgress, f as StampRecordResult, i as StampAward, n as LoyaltyModels, p as StampReversal, s as StampEntry, u as StampProgram } from "./index-CorB0JL6.mjs";
6
5
  import { PluginType, Repository } from "@classytic/mongokit";
7
6
  import { ResolvedTenantConfig } from "@classytic/repo-core/tenant";
8
7
  import { ClientSession, Model } from "mongoose";
@@ -12,6 +11,74 @@ import { ApprovalChain } from "@classytic/primitives/approval";
12
11
  //#region src/domain/ports/unit-of-work.port.d.ts
13
12
  interface UnitOfWork {
14
13
  withTransaction<T>(callback: (session: ClientSession) => Promise<T>): Promise<T>;
14
+ /**
15
+ * This unit of work MAY hand the callback a session with no live transaction.
16
+ *
17
+ * True only where the host opted into `allowNonTransactional` — a standalone
18
+ * mongod accepts the degradation knowingly, and loyalty logs it at boot. The
19
+ * guard that otherwise refuses a transaction-less session reads this, so an
20
+ * accepted degradation is not re-refused one layer down. Absent means NO:
21
+ * an implementation that yields a bare session without saying so is a defect,
22
+ * and stays one.
23
+ */
24
+ readonly allowsNonTransactional?: boolean;
25
+ }
26
+ //#endregion
27
+ //#region src/domain/program-settings.d.ts
28
+ interface EffectiveProgram {
29
+ pointValueMinor: number;
30
+ redemption: {
31
+ minPoints: number;
32
+ minOrderAmount: number;
33
+ maxRedeemPercent: number;
34
+ };
35
+ expiry: {
36
+ enabled: boolean;
37
+ defaultDays?: number;
38
+ };
39
+ }
40
+ //#endregion
41
+ //#region src/repositories/tier-definition.repository.d.ts
42
+ declare class TierDefinitionRepository extends Repository<TierDefinition> {
43
+ memberRepo: MemberRepository;
44
+ events: EventTransport;
45
+ outbox: OutboxStore | undefined;
46
+ notifications: NotificationAdapter | null;
47
+ config: ResolvedConfig;
48
+ constructor(model: Model<TierDefinition>, plugins?: PluginType[]);
49
+ /** Auto-inject tenant id from options into doc payload. See MemberRepository. */
50
+ create(data: Parameters<Repository<TierDefinition>['create']>[0], options?: Parameters<Repository<TierDefinition>['create']>[1]): Promise<TierDefinition>;
51
+ createMany(docs: Parameters<Repository<TierDefinition>['createMany']>[0], options?: Parameters<Repository<TierDefinition>['createMany']>[1]): Promise<TierDefinition[]>;
52
+ /** Session-bound outbox save + transport publish (PACKAGE_RULES §5.5 + P8). */
53
+ private _dispatch;
54
+ /** The highest tier `lifetime` points qualify for in a program, or null. */
55
+ qualifiedTierName(programId: string, lifetime: number, tc: Record<string, unknown>): Promise<string | null>;
56
+ /**
57
+ * Re-place a member by lifetime points. `upgradeOnly` is the earn path: a credit
58
+ * may promote at once, while a downgrade waits for the periodic evaluation.
59
+ */
60
+ evaluateMember(memberId: string, ctx: LoyaltyContext, options?: {
61
+ upgradeOnly?: boolean;
62
+ }): Promise<TierEvaluation>;
63
+ evaluateAll(ctx: LoyaltyContext): Promise<TierEvaluationBatch>;
64
+ setOverride(memberId: string, tierName: string, reason: string, ctx: LoyaltyContext, options?: {
65
+ expiresAt?: Date;
66
+ }): Promise<Member>;
67
+ clearOverride(memberId: string, ctx: LoyaltyContext): Promise<TierEvaluation>;
68
+ /**
69
+ * Sweeps members whose `tierOverrideExpiresAt` is past `now` and clears
70
+ * the override. Emits `TIER_OVERRIDE_EXPIRED` per member — distinct from
71
+ * the explicit `TIER_OVERRIDE_CLEARED` event — so audit logs can tell
72
+ * admin-initiated clears from TTL sweeps.
73
+ *
74
+ * Hosts call this on a cron schedule (e.g. hourly). Returns the number
75
+ * of overrides cleared. No-op when nothing matches.
76
+ */
77
+ sweepExpiredOverrides(ctx: LoyaltyContext, now?: Date): Promise<{
78
+ cleared: number;
79
+ }>;
80
+ private applyUpgrade;
81
+ private applyDowngrade;
15
82
  }
16
83
  //#endregion
17
84
  //#region src/repositories/member.repository.d.ts
@@ -20,6 +87,7 @@ declare class MemberRepository extends Repository<Member> {
20
87
  outbox: OutboxStore | undefined;
21
88
  notifications: NotificationAdapter | null;
22
89
  config: ResolvedConfig;
90
+ tierRepo: TierDefinitionRepository;
23
91
  /** Session-bound outbox save + transport publish (PACKAGE_RULES §5.5 + P8). */
24
92
  private _dispatch;
25
93
  constructor(model: Model<Member>, plugins?: PluginType[]);
@@ -62,7 +130,10 @@ declare class PointTransactionRepository extends Repository<PointTransaction> {
62
130
  notifications: NotificationAdapter | null;
63
131
  unitOfWork: UnitOfWork;
64
132
  config: ResolvedConfig;
133
+ program: () => Promise<EffectiveProgram>;
65
134
  constructor(model: Model<PointTransaction>, plugins?: PluginType[]);
135
+ /** `now + expiry.defaultDays` when the program expires points, else none. */
136
+ private defaultExpiry;
66
137
  /** Session-bound outbox save + transport publish (PACKAGE_RULES §5.5 + P8). */
67
138
  private _dispatch;
68
139
  private get _dispatchDeps();
@@ -75,6 +146,8 @@ declare class PointTransactionRepository extends Repository<PointTransaction> {
75
146
  earnPoints(input: {
76
147
  memberId: string;
77
148
  points: number;
149
+ /** Why the points are granted — required, closed set (`EARN_SOURCES`). */
150
+ source: EarnSource;
78
151
  description: string;
79
152
  referenceType?: string;
80
153
  referenceId?: string;
@@ -127,6 +200,29 @@ declare class PointTransactionRepository extends Repository<PointTransaction> {
127
200
  idempotencyKey?: string;
128
201
  metadata?: Record<string, unknown>;
129
202
  }, ctx: LoyaltyContext): Promise<PointTransaction>;
203
+ /**
204
+ * Take back points earned on a reference (an order) once it is refunded or canceled.
205
+ *
206
+ * CONVERGES BY STATE. The caller says how much of the purchase is undone
207
+ * (`fraction`, 1 = all); this reverses `floor(earned × fraction)` minus what earlier
208
+ * reversals already claimed. A replayed event reverses nothing extra, and the
209
+ * idempotency key is derived from the target so concurrent callers collide on the
210
+ * unique index instead of both debiting.
211
+ *
212
+ * A member cannot go below zero: points already spent are reported as `shortfall`
213
+ * on the row and the event. `lifetime` drops by the full claim — tier status must
214
+ * not keep credit for a sale that was undone.
215
+ */
216
+ reverseEarned(input: {
217
+ referenceType: string;
218
+ referenceId: string;
219
+ fraction: number;
220
+ reason: string;
221
+ }, ctx: LoyaltyContext): Promise<EarnReversalResult[]>;
222
+ /** Per member: points earned on the reference, and points earlier reversals already claimed. */
223
+ private tallyReference;
224
+ /** Debit one member's `delta` in its own unit of work; a lost race on the target key has converged. */
225
+ private claimBack;
130
226
  processExpirations(ctx: LoyaltyContext): Promise<ExpirationResult>;
131
227
  }
132
228
  //#endregion
@@ -134,6 +230,7 @@ declare class PointTransactionRepository extends Repository<PointTransaction> {
134
230
  declare class EarningRuleRepository extends Repository<EarningRule> {
135
231
  memberRepo: MemberRepository;
136
232
  txRepo: PointTransactionRepository;
233
+ tierRepo: TierDefinitionRepository;
137
234
  config: ResolvedConfig;
138
235
  constructor(model: Model<EarningRule>, plugins?: PluginType[]);
139
236
  private applyCreateDefaults;
@@ -162,6 +259,12 @@ declare class EarningRuleRepository extends Repository<EarningRule> {
162
259
  quantity: number;
163
260
  }>;
164
261
  }, ctx: LoyaltyContext): Promise<EarningResult>;
262
+ /**
263
+ * The member's tier benefit `benefits.pointsMultiplier`: above 1, the order earns
264
+ * (m − 1) × what the rules awarded, as its own ledger row. Keyed on the order
265
+ * alone, so a retry after a tier change cannot award a second bonus.
266
+ */
267
+ private tierMultiplierBonus;
165
268
  evaluateAction(input: {
166
269
  memberId: string;
167
270
  action: string;
@@ -254,6 +357,60 @@ interface Redemption {
254
357
  updatedAt: Date;
255
358
  }
256
359
  //#endregion
360
+ //#region src/domain/ports/redemption.port.d.ts
361
+ /**
362
+ * The points-redemption contract a checkout pipeline drives: reserve before the
363
+ * order is written, then confirm or release.
364
+ *
365
+ * Declared by the kernel that owns redemption so every channel (till, web)
366
+ * speaks one contract; pipelines `import type` it, adding nothing at runtime.
367
+ *
368
+ * The host binds it per request, so there is no context parameter. `reserve`
369
+ * DEBITS the balance: the caller must `release` on every failure path before the
370
+ * order exists, or the member loses points to an order that never did.
371
+ */
372
+ /** A reserved redemption, as a pipeline needs it. */
373
+ interface LoyaltyRedemption {
374
+ /** Redemption id — needed to confirm or release. */
375
+ id: string;
376
+ /** Discount granted, integer MINOR units (same unit as order totals). */
377
+ discountAmount: number;
378
+ /** Points actually held — the kernel may cap the request at the redeemable maximum. */
379
+ pointsReserved?: number;
380
+ }
381
+ interface LoyaltyReserveInput {
382
+ memberId: string;
383
+ pointsToRedeem: number;
384
+ /** Amount the redemption is computed against, integer minor units. */
385
+ orderTotal: number;
386
+ ownerType: 'order';
387
+ /** Stable owner id, so a retry reuses the hold instead of spending twice. */
388
+ ownerId: string;
389
+ }
390
+ /** Who a redemption is held against. */
391
+ interface RedemptionOwner {
392
+ ownerType: string;
393
+ ownerId: string;
394
+ }
395
+ /** The confirm-time handover: the hold moves from the owner it was reserved under to the one it paid for. */
396
+ interface RedemptionOwnerRebind {
397
+ /** The CURRENT owner — the confirm CAS refuses any other. */
398
+ from: RedemptionOwner;
399
+ to: RedemptionOwner;
400
+ }
401
+ interface LoyaltyRedemptionPort {
402
+ /** The member id behind a customer, or null when they are not enrolled. */
403
+ findMemberId(customerId: string): Promise<string | null>;
404
+ reserve(input: LoyaltyReserveInput): Promise<LoyaltyRedemption>;
405
+ /**
406
+ * Finalize points already debited at reserve time. `owner`: hand the hold to the
407
+ * placed order (from the owner it was reserved under); absent ⇒ the reserve-time owner stays.
408
+ */
409
+ confirm(redemptionId: string, owner?: RedemptionOwnerRebind): Promise<void>;
410
+ /** Return the points. MUST be best-effort — never masks the original error. */
411
+ release(redemptionId: string): Promise<void>;
412
+ }
413
+ //#endregion
257
414
  //#region src/repositories/redemption.repository.d.ts
258
415
  declare class RedemptionRepository extends Repository<Redemption> {
259
416
  memberRepo: MemberRepository;
@@ -263,6 +420,7 @@ declare class RedemptionRepository extends Repository<Redemption> {
263
420
  notifications: NotificationAdapter | null;
264
421
  unitOfWork: UnitOfWork;
265
422
  config: ResolvedConfig;
423
+ program: () => Promise<EffectiveProgram>;
266
424
  constructor(model: Model<Redemption>, plugins?: PluginType[]);
267
425
  /** Auto-inject tenant id from options into doc payload. See MemberRepository. */
268
426
  create(data: Parameters<Repository<Redemption>['create']>[0], options?: Parameters<Repository<Redemption>['create']>[1]): Promise<Redemption>;
@@ -270,6 +428,12 @@ declare class RedemptionRepository extends Repository<Redemption> {
270
428
  /** Session-bound outbox save + transport publish (PACKAGE_RULES §5.5 + P8). */
271
429
  private _dispatch;
272
430
  private get _dispatchDeps();
431
+ /**
432
+ * The points the program owes right now: every member's spendable balance plus the points
433
+ * held by open reservations (a reservation has left the balance but not yet been used), at
434
+ * the program's current point value. The measure a ledger's liability reconciles against.
435
+ */
436
+ outstandingLiability(ctx: LoyaltyContext): Promise<LoyaltyLiabilitySnapshot>;
273
437
  /**
274
438
  * Atomic compare-and-set status transition.
275
439
  *
@@ -289,7 +453,9 @@ declare class RedemptionRepository extends Repository<Redemption> {
289
453
  *
290
454
  * Anything that throws in here is a real error and must propagate.
291
455
  */
292
- atomicStatusTransition(id: string, expectedStatus: string, data: Record<string, unknown>, ctx?: Record<string, unknown>): Promise<Redemption | null>;
456
+ atomicStatusTransition(id: string, expectedStatus: string, data: Record<string, unknown>, ctx?: Record<string, unknown>,
457
+ /** Extra predicates the CAS must also match (e.g. the current owner of a rebind). */
458
+ guard?: Record<string, unknown>): Promise<Redemption | null>;
293
459
  /**
294
460
  * Validates a redemption request. `orderTotal` is an **integer in the
295
461
  * host's minor currency unit** (paisa for BDT); the returned
@@ -314,7 +480,20 @@ declare class RedemptionRepository extends Repository<Redemption> {
314
480
  ownerId: string;
315
481
  expiresAt?: Date;
316
482
  }, ctx: LoyaltyContext): Promise<Redemption>;
317
- confirm(redemptionId: string, ctx: LoyaltyContext): Promise<Redemption>;
483
+ /**
484
+ * `ownerExists: true` skips the reservation-expiry refusal — for a caller that
485
+ * knows the order was committed, where refusing would return points the customer
486
+ * already spent as a discount.
487
+ *
488
+ * `rebind` hands the hold to the document it paid for (a checkout reserves under its
489
+ * request key, the placed order owns it), inside the same CAS as the confirm; the CAS
490
+ * also matches `rebind.from`, so a caller holding only an id cannot re-home another
491
+ * owner's points. Absent ⇒ the reserve-time owner stays.
492
+ */
493
+ confirm(redemptionId: string, ctx: LoyaltyContext, options?: {
494
+ ownerExists?: boolean;
495
+ rebind?: RedemptionOwnerRebind;
496
+ }): Promise<Redemption>;
318
497
  release(redemptionId: string, ctx: LoyaltyContext): Promise<Redemption>;
319
498
  /**
320
499
  * Reverses a confirmed redemption. Credits points back to the member as
@@ -335,6 +514,36 @@ declare class RedemptionRepository extends Repository<Redemption> {
335
514
  proportional?: boolean;
336
515
  reason?: string;
337
516
  }): Promise<Redemption>;
517
+ /**
518
+ * Confirm every redemption still `reserved` against an owner that now exists.
519
+ *
520
+ * The convergence net for a checkout whose inline confirm failed after the order
521
+ * was committed: left `reserved`, the expiry sweep would RELEASE it and the
522
+ * member would keep both the discount and the points. Safe to repeat — a status
523
+ * CAS, and a redemption another caller already settled is skipped.
524
+ */
525
+ confirmForOwner(ownerIds: readonly string[], ctx: LoyaltyContext,
526
+ /** Hand each confirmed hold to this owner (the order), guarded on the owner it matched under. */
527
+ options?: {
528
+ rebindTo?: RedemptionOwner;
529
+ }): Promise<{
530
+ confirmed: number;
531
+ }>;
532
+ /**
533
+ * Undo every redemption held against an owner that was refunded or canceled:
534
+ * confirmed ones credit their points back, reserved ones release their hold.
535
+ *
536
+ * `ownerIds` lists every identity the owner was reserved under (a till reserves
537
+ * against its idempotency key, not the order number). Safe to repeat: each step is
538
+ * a status CAS, and a redemption another caller already settled is skipped.
539
+ */
540
+ undoForOwner(ownerIds: readonly string[], ctx: LoyaltyContext, options: {
541
+ reason: string;
542
+ }): Promise<{
543
+ reversed: number;
544
+ released: number;
545
+ pointsReturned: number;
546
+ }>;
338
547
  cleanupExpired(ctx: LoyaltyContext): Promise<number>;
339
548
  }
340
549
  //#endregion
@@ -401,39 +610,82 @@ declare class ReferralRepository extends Repository<Referral> {
401
610
  private requireReferral;
402
611
  }
403
612
  //#endregion
404
- //#region src/repositories/tier-definition.repository.d.ts
405
- declare class TierDefinitionRepository extends Repository<TierDefinition> {
613
+ //#region src/repositories/stamp-award.repository.d.ts
614
+ /** Awards are written only by `stamps.settle`; this repository is their read surface. */
615
+ declare class StampAwardRepository extends Repository<StampAward> {
616
+ config: ResolvedConfig;
617
+ constructor(model: Model<StampAward>, plugins?: PluginType[]);
618
+ create(data: Parameters<Repository<StampAward>['create']>[0], options?: Parameters<Repository<StampAward>['create']>[1]): Promise<StampAward>;
619
+ }
620
+ //#endregion
621
+ //#region src/repositories/stamp-program.repository.d.ts
622
+ /**
623
+ * Stamp-card programs. Every write is judged whole by `assertStampProgram` — a create, and a patch
624
+ * merged over the stored program — so no path (HTTP or direct) stores a program that cannot award.
625
+ */
626
+ declare class StampProgramRepository extends Repository<StampProgram> {
627
+ config: ResolvedConfig;
628
+ rewardIssuer: StampRewardIssuer | undefined;
629
+ constructor(model: Model<StampProgram>, plugins?: PluginType[]);
630
+ create(data: Parameters<Repository<StampProgram>['create']>[0], options?: Parameters<Repository<StampProgram>['create']>[1]): Promise<StampProgram>;
631
+ createMany(docs: Parameters<Repository<StampProgram>['createMany']>[0], options?: Parameters<Repository<StampProgram>['createMany']>[1]): Promise<StampProgram[]>;
632
+ /** A flat patch only: nested `earn` / `reward` are replaced whole, then the result is judged. */
633
+ update(id: Parameters<Repository<StampProgram>['update']>[0], data: Parameters<Repository<StampProgram>['update']>[1], options?: Parameters<Repository<StampProgram>['update']>[2]): Promise<StampProgram | null>;
634
+ }
635
+ //#endregion
636
+ //#region src/repositories/stamp.repository.d.ts
637
+ /**
638
+ * Stamp-card ledger: one entry per (program, member, order), consumed oldest-first into awards.
639
+ *
640
+ * Every guard is an index or a CAS filter, never an `if` above a write: the order entry, the visit
641
+ * day, the award sequence and each entry's `consumed` count.
642
+ */
643
+ declare class StampRepository extends Repository<StampEntry> {
406
644
  memberRepo: MemberRepository;
645
+ txRepo: PointTransactionRepository;
646
+ programRepo: StampProgramRepository;
647
+ awardRepo: StampAwardRepository;
407
648
  events: EventTransport;
408
649
  outbox: OutboxStore | undefined;
409
- notifications: NotificationAdapter | null;
650
+ unitOfWork: UnitOfWork;
410
651
  config: ResolvedConfig;
411
- constructor(model: Model<TierDefinition>, plugins?: PluginType[]);
412
- /** Auto-inject tenant id from options into doc payload. See MemberRepository. */
413
- create(data: Parameters<Repository<TierDefinition>['create']>[0], options?: Parameters<Repository<TierDefinition>['create']>[1]): Promise<TierDefinition>;
414
- createMany(docs: Parameters<Repository<TierDefinition>['createMany']>[0], options?: Parameters<Repository<TierDefinition>['createMany']>[1]): Promise<TierDefinition[]>;
415
- /** Session-bound outbox save + transport publish (PACKAGE_RULES §5.5 + P8). */
416
- private _dispatch;
417
- evaluateMember(memberId: string, ctx: LoyaltyContext): Promise<TierEvaluation>;
418
- evaluateAll(ctx: LoyaltyContext): Promise<TierEvaluationBatch>;
419
- setOverride(memberId: string, tierName: string, reason: string, ctx: LoyaltyContext, options?: {
420
- expiresAt?: Date;
421
- }): Promise<Member>;
422
- clearOverride(memberId: string, ctx: LoyaltyContext): Promise<TierEvaluation>;
652
+ rewardIssuer: StampRewardIssuer | undefined;
653
+ constructor(model: Model<StampEntry>, plugins?: PluginType[]);
654
+ private get deps();
655
+ private tc;
656
+ create(data: Parameters<Repository<StampEntry>['create']>[0], options?: Parameters<Repository<StampEntry>['create']>[1]): Promise<StampEntry>;
423
657
  /**
424
- * Sweeps members whose `tierOverrideExpiresAt` is past `now` and clears
425
- * the override. Emits `TIER_OVERRIDE_EXPIRED` per member — distinct from
426
- * the explicit `TIER_OVERRIDE_CLEARED` event — so audit logs can tell
427
- * admin-initiated clears from TTL sweeps.
428
- *
429
- * Hosts call this on a cron schedule (e.g. hourly). Returns the number
430
- * of overrides cleared. No-op when nothing matches.
658
+ * Record one paid order against every active stamp program of the member's loyalty program,
659
+ * then settle each. Re-recording the same order is a no-op that still settles, so a redelivery
660
+ * after a crash finishes the award the first delivery started.
431
661
  */
432
- sweepExpiredOverrides(ctx: LoyaltyContext, now?: Date): Promise<{
433
- cleared: number;
434
- }>;
435
- private applyUpgrade;
436
- private applyDowngrade;
662
+ recordOrder(input: StampOrderInput, ctx: LoyaltyContext): Promise<StampRecordResult[]>;
663
+ /** Insert the order's entry; `false` when the unique index says it (or its visit day) exists. */
664
+ private insertEntry;
665
+ /**
666
+ * Exchange full cards for rewards until the member's stamps (or `maxRewardsPerMember`) run out.
667
+ * Voucher awards claimed earlier but not yet issued are resumed first.
668
+ */
669
+ settle(stampProgramId: string, memberId: string, ctx: LoyaltyContext): Promise<StampAward[]>;
670
+ /**
671
+ * ONE award in ONE unit of work: consume the oldest stamps (CAS per entry), insert the award at
672
+ * the next sequence (unique — the race fence), and for points credit them in the same
673
+ * transaction. A voucher award commits as `pending`; its issuance happens after commit.
674
+ */
675
+ private claimAward;
676
+ /** Issue a claimed voucher award through the host port (idempotent on `awardKey`), then mark it. */
677
+ private issueVoucher;
678
+ private dispatchAwardIssued;
679
+ /**
680
+ * Reverse an undone order's stamps. Policy: unconsumed stamps are removed; stamps already
681
+ * consumed into an award are kept (`retainedInAwards`, on the result and the event) — never clawed back.
682
+ */
683
+ reverseOrder(orderRef: string, ctx: LoyaltyContext): Promise<StampReversal[]>;
684
+ private reverseEntry;
685
+ /** The member's card on every active program of their loyalty program. */
686
+ progress(memberId: string, ctx: LoyaltyContext): Promise<StampProgress[]>;
687
+ /** Unreversed, unexpired entries with stamps left, oldest first. */
688
+ private openEntries;
437
689
  }
438
690
  //#endregion
439
691
  //#region src/repositories/index.d.ts
@@ -444,6 +696,10 @@ interface LoyaltyRepositories {
444
696
  tierDefinition: TierDefinitionRepository;
445
697
  redemption: RedemptionRepository;
446
698
  referral: ReferralRepository;
699
+ stampProgram: StampProgramRepository;
700
+ /** Stamp-card verbs: `recordOrder`, `settle`, `reverseOrder`, `progress`. */
701
+ stamps: StampRepository;
702
+ stampAward: StampAwardRepository;
447
703
  }
448
704
  interface CreateRepositoriesInput {
449
705
  models: LoyaltyModels;
@@ -454,7 +710,11 @@ interface CreateRepositoriesInput {
454
710
  outbox?: OutboxStore | undefined;
455
711
  notifications: NotificationAdapter | null;
456
712
  unitOfWork: UnitOfWork;
713
+ /** Per-operation program settings — see `createProgramSettings`. */
714
+ programSettings: () => Promise<EffectiveProgram>;
715
+ /** Host port for stamp-card voucher rewards. */
716
+ rewardIssuer?: StampRewardIssuer | undefined;
457
717
  }
458
718
  declare function createRepositories(input: CreateRepositoriesInput): LoyaltyRepositories;
459
719
  //#endregion
460
- export { ReferralRepository as a, Redemption as c, MemberRepository as d, UnitOfWork as f, TierDefinitionRepository as i, EarningRuleRepository as l, LoyaltyRepositories as n, Referral as o, createRepositories as r, RedemptionRepository as s, CreateRepositoriesInput as t, PointTransactionRepository as u };
720
+ export { PointTransactionRepository as _, StampProgramRepository as a, UnitOfWork as b, Referral as c, LoyaltyRedemptionPort as d, LoyaltyReserveInput as f, EarningRuleRepository as g, Redemption as h, StampRepository as i, RedemptionRepository as l, RedemptionOwnerRebind as m, LoyaltyRepositories as n, StampAwardRepository as o, RedemptionOwner as p, createRepositories as r, ReferralRepository as s, CreateRepositoriesInput as t, LoyaltyRedemption as u, MemberRepository as v, TierDefinitionRepository as y };