@classytic/loyalty 0.7.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 +134 -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-BIPHUsfR.d.mts → index-BvyzKU7R.d.mts} +283 -34
  13. package/dist/{index-BIhVuwIZ.d.mts → index-CorB0JL6.d.mts} +255 -109
  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 +59 -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-DErtHQou.mjs → repositories-ER68JmUo.mjs} +1161 -99
  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-BIhVuwIZ.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";
@@ -25,12 +24,70 @@ interface UnitOfWork {
25
24
  readonly allowsNonTransactional?: boolean;
26
25
  }
27
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;
82
+ }
83
+ //#endregion
28
84
  //#region src/repositories/member.repository.d.ts
29
85
  declare class MemberRepository extends Repository<Member> {
30
86
  events: EventTransport;
31
87
  outbox: OutboxStore | undefined;
32
88
  notifications: NotificationAdapter | null;
33
89
  config: ResolvedConfig;
90
+ tierRepo: TierDefinitionRepository;
34
91
  /** Session-bound outbox save + transport publish (PACKAGE_RULES §5.5 + P8). */
35
92
  private _dispatch;
36
93
  constructor(model: Model<Member>, plugins?: PluginType[]);
@@ -73,7 +130,10 @@ declare class PointTransactionRepository extends Repository<PointTransaction> {
73
130
  notifications: NotificationAdapter | null;
74
131
  unitOfWork: UnitOfWork;
75
132
  config: ResolvedConfig;
133
+ program: () => Promise<EffectiveProgram>;
76
134
  constructor(model: Model<PointTransaction>, plugins?: PluginType[]);
135
+ /** `now + expiry.defaultDays` when the program expires points, else none. */
136
+ private defaultExpiry;
77
137
  /** Session-bound outbox save + transport publish (PACKAGE_RULES §5.5 + P8). */
78
138
  private _dispatch;
79
139
  private get _dispatchDeps();
@@ -86,6 +146,8 @@ declare class PointTransactionRepository extends Repository<PointTransaction> {
86
146
  earnPoints(input: {
87
147
  memberId: string;
88
148
  points: number;
149
+ /** Why the points are granted — required, closed set (`EARN_SOURCES`). */
150
+ source: EarnSource;
89
151
  description: string;
90
152
  referenceType?: string;
91
153
  referenceId?: string;
@@ -138,6 +200,29 @@ declare class PointTransactionRepository extends Repository<PointTransaction> {
138
200
  idempotencyKey?: string;
139
201
  metadata?: Record<string, unknown>;
140
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;
141
226
  processExpirations(ctx: LoyaltyContext): Promise<ExpirationResult>;
142
227
  }
143
228
  //#endregion
@@ -145,6 +230,7 @@ declare class PointTransactionRepository extends Repository<PointTransaction> {
145
230
  declare class EarningRuleRepository extends Repository<EarningRule> {
146
231
  memberRepo: MemberRepository;
147
232
  txRepo: PointTransactionRepository;
233
+ tierRepo: TierDefinitionRepository;
148
234
  config: ResolvedConfig;
149
235
  constructor(model: Model<EarningRule>, plugins?: PluginType[]);
150
236
  private applyCreateDefaults;
@@ -173,6 +259,12 @@ declare class EarningRuleRepository extends Repository<EarningRule> {
173
259
  quantity: number;
174
260
  }>;
175
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;
176
268
  evaluateAction(input: {
177
269
  memberId: string;
178
270
  action: string;
@@ -265,6 +357,60 @@ interface Redemption {
265
357
  updatedAt: Date;
266
358
  }
267
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
268
414
  //#region src/repositories/redemption.repository.d.ts
269
415
  declare class RedemptionRepository extends Repository<Redemption> {
270
416
  memberRepo: MemberRepository;
@@ -274,6 +420,7 @@ declare class RedemptionRepository extends Repository<Redemption> {
274
420
  notifications: NotificationAdapter | null;
275
421
  unitOfWork: UnitOfWork;
276
422
  config: ResolvedConfig;
423
+ program: () => Promise<EffectiveProgram>;
277
424
  constructor(model: Model<Redemption>, plugins?: PluginType[]);
278
425
  /** Auto-inject tenant id from options into doc payload. See MemberRepository. */
279
426
  create(data: Parameters<Repository<Redemption>['create']>[0], options?: Parameters<Repository<Redemption>['create']>[1]): Promise<Redemption>;
@@ -281,6 +428,12 @@ declare class RedemptionRepository extends Repository<Redemption> {
281
428
  /** Session-bound outbox save + transport publish (PACKAGE_RULES §5.5 + P8). */
282
429
  private _dispatch;
283
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>;
284
437
  /**
285
438
  * Atomic compare-and-set status transition.
286
439
  *
@@ -300,7 +453,9 @@ declare class RedemptionRepository extends Repository<Redemption> {
300
453
  *
301
454
  * Anything that throws in here is a real error and must propagate.
302
455
  */
303
- 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>;
304
459
  /**
305
460
  * Validates a redemption request. `orderTotal` is an **integer in the
306
461
  * host's minor currency unit** (paisa for BDT); the returned
@@ -325,7 +480,20 @@ declare class RedemptionRepository extends Repository<Redemption> {
325
480
  ownerId: string;
326
481
  expiresAt?: Date;
327
482
  }, ctx: LoyaltyContext): Promise<Redemption>;
328
- 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>;
329
497
  release(redemptionId: string, ctx: LoyaltyContext): Promise<Redemption>;
330
498
  /**
331
499
  * Reverses a confirmed redemption. Credits points back to the member as
@@ -346,6 +514,36 @@ declare class RedemptionRepository extends Repository<Redemption> {
346
514
  proportional?: boolean;
347
515
  reason?: string;
348
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
+ }>;
349
547
  cleanupExpired(ctx: LoyaltyContext): Promise<number>;
350
548
  }
351
549
  //#endregion
@@ -412,39 +610,82 @@ declare class ReferralRepository extends Repository<Referral> {
412
610
  private requireReferral;
413
611
  }
414
612
  //#endregion
415
- //#region src/repositories/tier-definition.repository.d.ts
416
- 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> {
417
644
  memberRepo: MemberRepository;
645
+ txRepo: PointTransactionRepository;
646
+ programRepo: StampProgramRepository;
647
+ awardRepo: StampAwardRepository;
418
648
  events: EventTransport;
419
649
  outbox: OutboxStore | undefined;
420
- notifications: NotificationAdapter | null;
650
+ unitOfWork: UnitOfWork;
421
651
  config: ResolvedConfig;
422
- constructor(model: Model<TierDefinition>, plugins?: PluginType[]);
423
- /** Auto-inject tenant id from options into doc payload. See MemberRepository. */
424
- create(data: Parameters<Repository<TierDefinition>['create']>[0], options?: Parameters<Repository<TierDefinition>['create']>[1]): Promise<TierDefinition>;
425
- createMany(docs: Parameters<Repository<TierDefinition>['createMany']>[0], options?: Parameters<Repository<TierDefinition>['createMany']>[1]): Promise<TierDefinition[]>;
426
- /** Session-bound outbox save + transport publish (PACKAGE_RULES §5.5 + P8). */
427
- private _dispatch;
428
- evaluateMember(memberId: string, ctx: LoyaltyContext): Promise<TierEvaluation>;
429
- evaluateAll(ctx: LoyaltyContext): Promise<TierEvaluationBatch>;
430
- setOverride(memberId: string, tierName: string, reason: string, ctx: LoyaltyContext, options?: {
431
- expiresAt?: Date;
432
- }): Promise<Member>;
433
- 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>;
434
657
  /**
435
- * Sweeps members whose `tierOverrideExpiresAt` is past `now` and clears
436
- * the override. Emits `TIER_OVERRIDE_EXPIRED` per member — distinct from
437
- * the explicit `TIER_OVERRIDE_CLEARED` event — so audit logs can tell
438
- * admin-initiated clears from TTL sweeps.
439
- *
440
- * Hosts call this on a cron schedule (e.g. hourly). Returns the number
441
- * 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.
442
661
  */
443
- sweepExpiredOverrides(ctx: LoyaltyContext, now?: Date): Promise<{
444
- cleared: number;
445
- }>;
446
- private applyUpgrade;
447
- 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;
448
689
  }
449
690
  //#endregion
450
691
  //#region src/repositories/index.d.ts
@@ -455,6 +696,10 @@ interface LoyaltyRepositories {
455
696
  tierDefinition: TierDefinitionRepository;
456
697
  redemption: RedemptionRepository;
457
698
  referral: ReferralRepository;
699
+ stampProgram: StampProgramRepository;
700
+ /** Stamp-card verbs: `recordOrder`, `settle`, `reverseOrder`, `progress`. */
701
+ stamps: StampRepository;
702
+ stampAward: StampAwardRepository;
458
703
  }
459
704
  interface CreateRepositoriesInput {
460
705
  models: LoyaltyModels;
@@ -465,7 +710,11 @@ interface CreateRepositoriesInput {
465
710
  outbox?: OutboxStore | undefined;
466
711
  notifications: NotificationAdapter | null;
467
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;
468
717
  }
469
718
  declare function createRepositories(input: CreateRepositoriesInput): LoyaltyRepositories;
470
719
  //#endregion
471
- 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 };