@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
@@ -0,0 +1,604 @@
1
+ import { f as TransactionType, o as RuleStatus, r as MemberStatus, s as RuleType, t as EarnSource } from "./index-tjMMmPzU.mjs";
2
+ import { t as NotificationAdapter } from "./notification.port-wY4IKimz.mjs";
3
+ import { ResolvedTenantConfig, TenantConfig } from "@classytic/repo-core/tenant";
4
+ import { EventTransport } from "@classytic/primitives/events";
5
+ import { OperationContext } from "@classytic/primitives/context";
6
+ import { OutboxStore } from "@classytic/primitives/outbox";
7
+ import { ApprovalChain } from "@classytic/primitives/approval";
8
+ //#region src/domain/entities/earning-rule.d.ts
9
+ interface EarningConditions {
10
+ /**
11
+ * Minimum order total for the rule to apply — **integer minor units** of the
12
+ * host's ambient currency (paisa for BDT). `10000` = ৳100. Compared against
13
+ * `evaluateOrder`'s `orderTotal` (also minor units).
14
+ */
15
+ minOrderAmount?: number;
16
+ categories?: string[];
17
+ tiers?: string[];
18
+ actions?: string[];
19
+ /**
20
+ * Days the rule applies, JS `getDay()` convention: 0=Sun … 6=Sat. Empty /
21
+ * omitted = every day. Evaluated against the wall-clock weekday in the
22
+ * engine's configured `timezone` (UTC when unset), never the server's local
23
+ * clock. See `LoyaltyShape.timezone`.
24
+ */
25
+ dayOfWeek?: number[];
26
+ dateRange?: {
27
+ start?: Date;
28
+ end?: Date;
29
+ };
30
+ }
31
+ interface EarningReward {
32
+ /**
33
+ * Points awarded per `rateBasis` minor units of spend:
34
+ * `points = pointsPerAmount × (orderTotalMinor / rateBasis)` (rounded once
35
+ * per `roundingMode`). REQUIRES `rateBasis` — there is no implicit
36
+ * "per 1 major unit" basis anymore (that ambiguity was the bug).
37
+ * "5 points per ৳100" = `{ pointsPerAmount: 5, rateBasis: 10000 }`.
38
+ */
39
+ pointsPerAmount?: number;
40
+ /**
41
+ * RATE: the explicit integer minor-unit basis `pointsPerAmount` is quoted
42
+ * against (≥ 1). Required whenever `pointsPerAmount` is set.
43
+ */
44
+ rateBasis?: number;
45
+ /**
46
+ * RATE: integer minor units of spend per single point (≥ 1).
47
+ * `points = orderTotalMinor / amountPerPoint` (rounded per `roundingMode`).
48
+ * "1 point per ৳100" = `amountPerPoint: 10000`.
49
+ */
50
+ amountPerPoint?: number;
51
+ fixedPoints?: number;
52
+ multiplier?: number;
53
+ roundingMode?: 'floor' | 'round' | 'ceil';
54
+ maxPointsPerTransaction?: number;
55
+ }
56
+ interface EarningRule {
57
+ _id: string;
58
+ programId: string;
59
+ name: string;
60
+ description?: string;
61
+ type: RuleType;
62
+ status: RuleStatus;
63
+ priority: number;
64
+ conditions: EarningConditions;
65
+ reward: EarningReward;
66
+ startsAt?: Date;
67
+ endsAt?: Date;
68
+ metadata?: Record<string, unknown>;
69
+ createdAt: Date;
70
+ updatedAt: Date;
71
+ }
72
+ //#endregion
73
+ //#region src/domain/entities/tier-definition.d.ts
74
+ interface TierQualification {
75
+ minLifetimePoints?: number;
76
+ /**
77
+ * Minimum lifetime spend — **integer minor units** (paisa for BDT).
78
+ * Declared for host-side qualification logic only: the in-package tier
79
+ * evaluation checks lifetime POINTS exclusively and never reads this field.
80
+ */
81
+ minLifetimeSpend?: number;
82
+ evaluationPeriodDays?: number;
83
+ }
84
+ interface TierBenefits {
85
+ pointsMultiplier?: number;
86
+ discountPercent?: number;
87
+ freeShipping?: boolean;
88
+ customBenefits?: Record<string, unknown>;
89
+ }
90
+ interface TierDowngrade {
91
+ enabled: boolean;
92
+ gracePeriodDays: number;
93
+ }
94
+ interface TierDefinition {
95
+ _id: string;
96
+ programId: string;
97
+ name: string;
98
+ rank: number;
99
+ qualificationCriteria: TierQualification;
100
+ benefits: TierBenefits;
101
+ downgrade: TierDowngrade;
102
+ color?: string;
103
+ metadata?: Record<string, unknown>;
104
+ createdAt: Date;
105
+ updatedAt: Date;
106
+ }
107
+ //#endregion
108
+ //#region src/domain/ports/reward-issuer.port.d.ts
109
+ interface StampVoucherRequest {
110
+ voucherProgramId: string;
111
+ stampProgramId: string;
112
+ memberId: string;
113
+ /** The member's external subject — what the host keys its customers by. */
114
+ externalId: string;
115
+ externalType: string;
116
+ /** Deterministic per award: the SAME key must return the SAME voucher, however often it is retried. */
117
+ awardKey: string;
118
+ }
119
+ /**
120
+ * Issues a stamp card's non-points reward. Host-supplied (`LoyaltyRuntime.rewardIssuer`) — loyalty
121
+ * never imports a promo engine. Required by any program whose reward is a voucher: such a program
122
+ * is refused at write when none is bound.
123
+ */
124
+ interface StampRewardIssuer {
125
+ issueVoucher(request: StampVoucherRequest, ctx: LoyaltyContext): Promise<{
126
+ voucherId: string;
127
+ code: string;
128
+ }>;
129
+ }
130
+ //#endregion
131
+ //#region src/types/config.d.ts
132
+ type LoyaltyModelName = 'Member' | 'PointTransaction' | 'EarningRule' | 'TierDefinition' | 'Redemption' | 'Referral' | 'StampProgram' | 'StampEntry' | 'StampAward';
133
+ type TenantConfig$1 = TenantConfig | boolean;
134
+ /** @deprecated Use `ResolvedTenantConfig` from `@classytic/repo-core/tenant`. */
135
+ type ResolvedTenant = ResolvedTenantConfig;
136
+ interface IndexDefinition {
137
+ fields: Record<string, 1 | -1>;
138
+ options?: Record<string, unknown> | undefined;
139
+ }
140
+ type ModelName = 'member' | 'pointTransaction' | 'earningRule' | 'tierDefinition' | 'redemption' | 'referral' | 'stampProgram' | 'stampEntry' | 'stampAward';
141
+ type PluginType = unknown;
142
+ interface RepositoryPlugins {
143
+ member?: PluginType[] | undefined;
144
+ pointTransaction?: PluginType[] | undefined;
145
+ earningRule?: PluginType[] | undefined;
146
+ tierDefinition?: PluginType[] | undefined;
147
+ redemption?: PluginType[] | undefined;
148
+ referral?: PluginType[] | undefined;
149
+ stampProgram?: PluginType[] | undefined;
150
+ stampEntry?: PluginType[] | undefined;
151
+ stampAward?: PluginType[] | undefined;
152
+ }
153
+ interface LoyaltyShape {
154
+ tenant?: TenantConfig$1 | undefined;
155
+ /**
156
+ * IANA timezone name (e.g. `'Asia/Dhaka'`, `'America/New_York'`) used to
157
+ * evaluate wall-clock rule conditions — currently `EarningConditions.dayOfWeek`
158
+ * ("2× points on Fridays"). Resolved DST-exact per instant via
159
+ * `@classytic/primitives/timezone`. Validated at boot: an unknown zone throws
160
+ * `ValidationError`, never a silent mis-evaluation.
161
+ *
162
+ * Omitted → **UTC** (the day boundary is the UTC calendar day). Either way the
163
+ * result NEVER depends on the deploy machine's `TZ` env. PACKAGE_RULES P12.
164
+ */
165
+ timezone?: string | undefined;
166
+ program?: {
167
+ id?: string | undefined;
168
+ /**
169
+ * RATE: integer minor units ONE point redeems for (≥ 1) in the host's
170
+ * ambient currency. `pointValueMinor: 10` = 1 point → 10 paisa
171
+ * (৳0.10, i.e. the old `conversionRate: 10` "10 points per ৳1").
172
+ * Replaces `conversionRate` (0.5.x), whose implicit per-1-MAJOR-unit
173
+ * basis was ambiguous.
174
+ */
175
+ pointValueMinor?: number | undefined;
176
+ } | undefined;
177
+ redemption?: {
178
+ minPoints?: number | undefined;
179
+ /**
180
+ * Minimum order total eligible for redemption — **integer minor
181
+ * units** (paisa for BDT). Compared against `validate()`'s
182
+ * `orderTotal` (also minor units).
183
+ */
184
+ minOrderAmount?: number | undefined;
185
+ maxRedeemPercent?: number | undefined;
186
+ reservationTtlMinutes?: number | undefined;
187
+ } | undefined;
188
+ referral?: {
189
+ referrerRewardPoints?: number | undefined;
190
+ refereeRewardPoints?: number | undefined;
191
+ maxPerPeriod?: number | undefined;
192
+ periodDays?: number | undefined;
193
+ requireApproval?: boolean | undefined;
194
+ codeLength?: number | undefined;
195
+ /**
196
+ * Maximum ancestor depth for multi-tier referral rewards. When >= 2,
197
+ * the direct referrer gets `tierRates[0]` percent of
198
+ * `referrerRewardPoints`, the 2nd-tier ancestor gets `tierRates[1]`,
199
+ * etc. Depth 1 = direct referrer (matches single-hop legacy path).
200
+ * Default: 1 (single-hop, preserves prior behaviour).
201
+ */
202
+ maxDepth?: number | undefined;
203
+ /**
204
+ * Percent rates (0–100) per depth. `tierRates[0]` is the direct
205
+ * referrer's share. Omitted depths fall back to 0. Example:
206
+ * `{ maxDepth: 3, tierRates: [100, 50, 25] }` — direct gets 100%,
207
+ * grand-referrer 50%, great-grand 25%.
208
+ */
209
+ tierRates?: number[] | undefined;
210
+ } | undefined;
211
+ expiry?: {
212
+ enabled?: boolean | undefined;
213
+ defaultDays?: number | undefined;
214
+ } | undefined;
215
+ indexes?: Partial<Record<ModelName, IndexDefinition[]>> | undefined;
216
+ autoIndex?: boolean | Partial<Record<LoyaltyModelName, boolean>> | undefined;
217
+ /**
218
+ * Optional prefix prepended to every physical collection this package
219
+ * creates (see PACKAGE_RULES.md §20.1). Unset → default names
220
+ * (`loyalty_members`, `loyalty_point_transactions`, `loyalty_earning_rules`,
221
+ * `loyalty_tier_definitions`, `loyalty_redemptions`, `loyalty_referrals`).
222
+ * Model names and `ref:` populate are unaffected.
223
+ */
224
+ collectionPrefix?: string | undefined;
225
+ /**
226
+ * When true, existing Mongoose models with loyalty's names are deleted
227
+ * from the connection before re-registering. Hot-reload / test fixtures
228
+ * only. Default `false` — collision throws mongokit's `ModelCollisionError`.
229
+ * Hosts that need two loyalty engines should use two Mongoose connections
230
+ * (`mongoose.createConnection(...)`). See PACKAGE_RULES.md §21.
231
+ */
232
+ forceRecreate?: boolean | undefined;
233
+ }
234
+ /** Settings an operator changes while the program runs. Unset = the shape's value. */
235
+ interface LiveProgramSettings {
236
+ pointValueMinor?: number | undefined;
237
+ redemption?: {
238
+ minPoints?: number | undefined;
239
+ minOrderAmount?: number | undefined;
240
+ maxRedeemPercent?: number | undefined;
241
+ } | undefined;
242
+ /** Days until earned points expire; `null` switches expiry off. */
243
+ expiryDays?: number | null | undefined;
244
+ }
245
+ interface LoyaltyRuntime {
246
+ /**
247
+ * Arc-compatible event transport. When omitted, the engine creates (and
248
+ * OWNS, so `close()` closes) an `InProcessLoyaltyBus` — a ~60-line structural
249
+ * match of arc's `MemoryEventTransport`. A host-supplied transport is the
250
+ * host's to close. See PACKAGE_RULES §13.
251
+ */
252
+ eventTransport?: EventTransport | undefined;
253
+ /**
254
+ * Host-owned outbox store (PACKAGE_RULES §5.5 + P8). When present, every
255
+ * domain event is persisted via `outbox.save(event, { session })` **before**
256
+ * `eventTransport.publish(event)`. `session` is threaded from `ctx.session`
257
+ * so the outbox row commits atomically with the business write.
258
+ *
259
+ * Loyalty does NOT ship a store. Hosts wire arc's `repositoryAsOutboxStore(mongokitRepo)`
260
+ * over their own `event_outbox` collection, arc's `MemoryOutboxStore`, or any
261
+ * custom Postgres / Redis / Kafka implementation of the contract. When
262
+ * omitted, events flow through `eventTransport` only (no durable log).
263
+ */
264
+ outbox?: OutboxStore | undefined;
265
+ notifications?: NotificationAdapter | undefined;
266
+ plugins?: RepositoryPlugins | undefined;
267
+ /**
268
+ * The operator-owned program settings, read PER OPERATION. Supply it when an
269
+ * admin can change them while the program runs; the shape's values are the
270
+ * fallback for anything it leaves unset. Without it they are fixed at bind,
271
+ * so a saved change does nothing until restart.
272
+ */
273
+ programSettings?: (() => LiveProgramSettings | Promise<LiveProgramSettings>) | undefined;
274
+ /**
275
+ * Skip the boot-time capability gate that asserts the backend supports
276
+ * multi-document `transactions`. Loyalty's point-mutation verbs (earn,
277
+ * adjust, reserve, confirm, release, reverse, processExpirations) run inside
278
+ * `withTransaction`; without a replica set those writes are NOT atomic.
279
+ * Leave `false` (default) in production — set `true` only for local-dev on
280
+ * standalone Mongo, accepting partial-write risk. See PACKAGE_RULES
281
+ * "Runtime capabilities".
282
+ */
283
+ allowNonTransactional?: boolean | undefined;
284
+ /**
285
+ * Issues stamp-card voucher rewards. Required by any stamp program whose reward is a voucher —
286
+ * creating one on an engine bound without it is refused.
287
+ */
288
+ rewardIssuer?: StampRewardIssuer | undefined;
289
+ /**
290
+ * Where the engine's boot warnings go — today, the `allowNonTransactional`
291
+ * downgrade notice. Defaults to `console`. A downgrade that carries
292
+ * partial-write risk on stored value must be VISIBLE, and a host that routes
293
+ * logs elsewhere would otherwise never see it.
294
+ */
295
+ logger?: {
296
+ warn?(message: string, ...args: unknown[]): void;
297
+ error?(message: string, ...args: unknown[]): void;
298
+ } | undefined;
299
+ }
300
+ interface ResolvedConfig {
301
+ programId: string;
302
+ /** Integer minor units one point redeems for (≥ 1). See `LoyaltyShape.program.pointValueMinor`. */
303
+ pointValueMinor: number;
304
+ redemption: {
305
+ minPoints: number;
306
+ /** Integer minor units. */
307
+ minOrderAmount: number;
308
+ maxRedeemPercent: number;
309
+ reservationTtlMinutes: number;
310
+ };
311
+ referral: {
312
+ referrerRewardPoints: number;
313
+ refereeRewardPoints: number;
314
+ maxPerPeriod: number;
315
+ periodDays: number;
316
+ requireApproval: boolean;
317
+ codeLength: number;
318
+ maxDepth: number;
319
+ tierRates: number[];
320
+ };
321
+ expiry: {
322
+ enabled: boolean;
323
+ defaultDays?: number;
324
+ };
325
+ /** Resolved IANA zone for wall-clock rule evaluation; `undefined` = UTC. */
326
+ timezone?: string;
327
+ tenant: ResolvedTenantConfig;
328
+ }
329
+ //#endregion
330
+ //#region src/domain/entities/member.d.ts
331
+ interface MemberBalance {
332
+ current: number;
333
+ lifetime: number;
334
+ redeemed: number;
335
+ expired: number;
336
+ }
337
+ interface Member {
338
+ _id: string;
339
+ programId: string;
340
+ externalId: string;
341
+ externalType: string;
342
+ status: MemberStatus;
343
+ /** Public loyalty card identifier (e.g., MBR-DHK-00004821-8). Set by the integration layer. */
344
+ cardId?: string;
345
+ /** Referral code for the referral system (separate from cardId). Auto-generated on enrollment. */
346
+ referralCode: string;
347
+ tier: string | null;
348
+ tierOverride: string | null;
349
+ tierOverrideReason?: string;
350
+ /**
351
+ * Optional TTL on a manual tier override. When set, `evaluateMember`
352
+ * treats the override as already cleared once `tierOverrideExpiresAt <=
353
+ * now`, and a host-scheduled `sweepExpiredOverrides` cron clears the
354
+ * field + emits a `TIER_OVERRIDE_EXPIRED` audit event.
355
+ */
356
+ tierOverrideExpiresAt?: Date;
357
+ /** Optional upstream referrer for multi-tier referral chains. */
358
+ referredBy?: string;
359
+ tierQualifiedAt?: Date;
360
+ tierExpiresAt?: Date;
361
+ balance: MemberBalance;
362
+ enrolledAt: Date;
363
+ deactivatedAt?: Date;
364
+ metadata?: Record<string, unknown>;
365
+ createdAt: Date;
366
+ updatedAt: Date;
367
+ }
368
+ //#endregion
369
+ //#region src/domain/entities/point-transaction.d.ts
370
+ interface PointTransaction {
371
+ _id: string;
372
+ memberId: string;
373
+ programId: string;
374
+ type: TransactionType;
375
+ points: number;
376
+ balanceAfter: number;
377
+ description: string;
378
+ /** Earn rows: why the points were granted. */
379
+ source?: EarnSource;
380
+ /** Minor units one point was worth when this row was written. Absent on rows written before it existed. */
381
+ pointValueMinor?: number;
382
+ referenceType?: string;
383
+ referenceId?: string;
384
+ idempotencyKey?: string;
385
+ earningRuleId?: string;
386
+ expiresAt?: Date;
387
+ expiredAt?: Date;
388
+ metadata?: Record<string, unknown>;
389
+ actorId: string;
390
+ /**
391
+ * Optional multi-step approval chain (P7 canonical — same shape as
392
+ * `@classytic/purchase` / `@classytic/order`). Manual `adjustment` /
393
+ * `bonus` transactions above a host-defined threshold gate on
394
+ * `isApproved(approvals)` before the journal entry is committed.
395
+ * Automatic transactions (earn from purchase, expiry sweeps) skip the
396
+ * chain entirely — single-tier flows leave this null.
397
+ */
398
+ approvals?: ApprovalChain | null;
399
+ /** Snapshot of the matrix policy id that produced this chain (host-managed). */
400
+ approvalPolicyId?: string | null;
401
+ /** Version of the matrix policy at submit time — frozen for audit. */
402
+ approvalPolicyVersion?: number | null;
403
+ createdAt: Date;
404
+ }
405
+ //#endregion
406
+ //#region src/types/results.d.ts
407
+ interface BalanceSummary extends MemberBalance {
408
+ tier: string | null;
409
+ tierOverride: string | null;
410
+ }
411
+ interface EarningBreakdown {
412
+ ruleId: string;
413
+ ruleName: string;
414
+ points: number;
415
+ }
416
+ interface EarningResult {
417
+ totalPoints: number;
418
+ breakdown: EarningBreakdown[];
419
+ transactions: PointTransaction[];
420
+ }
421
+ interface TierEvaluation {
422
+ memberId: string;
423
+ previousTier: string | null;
424
+ newTier: string | null;
425
+ action: 'upgraded' | 'downgraded' | 'grace_period' | 'unchanged';
426
+ graceExpiresAt?: Date;
427
+ }
428
+ interface TierEvaluationBatch {
429
+ upgraded: number;
430
+ downgraded: number;
431
+ unchanged: number;
432
+ }
433
+ interface RedemptionValidation {
434
+ valid: boolean;
435
+ error?: string;
436
+ pointsToRedeem: number;
437
+ /**
438
+ * Discount in **integer minor units** (paisa for BDT) — always exactly
439
+ * `pointsToRedeem × pointValueMinor`.
440
+ */
441
+ discountAmount: number;
442
+ maxAllowedPoints: number;
443
+ }
444
+ /** What the program owes, in points and in integer minor units at the current point value. */
445
+ interface LoyaltyLiabilitySnapshot {
446
+ spendablePoints: number;
447
+ /** Held by open reservations — out of the balance, not yet used. */
448
+ reservedPoints: number;
449
+ outstandingPoints: number;
450
+ pointValueMinor: number;
451
+ valueMinor: number;
452
+ }
453
+ interface ExpirationResult {
454
+ membersAffected: number;
455
+ pointsExpired: number;
456
+ transactionCount: number;
457
+ }
458
+ /**
459
+ * Outcome of `PointTransactionRepository.reverseEarned` for one member.
460
+ * `claimed` is what the refund entitles the program to take back; `collected` is
461
+ * what was actually debited. A non-zero `shortfall` means the member had already
462
+ * spent those points — reported, never silently written off.
463
+ */
464
+ interface EarnReversalResult {
465
+ memberId: string | null;
466
+ earned: number;
467
+ /** Points that should stand reversed after this call (`floor(earned × fraction)`). */
468
+ target: number;
469
+ alreadyClaimed: number;
470
+ claimed: number;
471
+ collected: number;
472
+ shortfall: number;
473
+ transactionId: string | null;
474
+ }
475
+ interface PaginatedResult<T> {
476
+ docs: T[];
477
+ total: number;
478
+ page: number;
479
+ pages: number;
480
+ hasNext: boolean;
481
+ hasPrev: boolean;
482
+ }
483
+ interface ListQuery {
484
+ page?: number;
485
+ limit?: number;
486
+ sort?: Record<string, 1 | -1>;
487
+ }
488
+ //#endregion
489
+ //#region src/types/index.d.ts
490
+ /**
491
+ * Context passed to every loyalty operation.
492
+ *
493
+ * The tenant key is dynamic — if you configured `tenant: { field: 'companyId', contextKey: 'companyId' }`,
494
+ * pass `{ companyId: '...' }` in the context. The `[key: string]: unknown` index allows this.
495
+ *
496
+ * For the default config (`tenant: true` or `tenant: undefined`), pass `organizationId`.
497
+ * For `tenant: false`, the tenant key is ignored.
498
+ */
499
+ /**
500
+ * Extends `@classytic/primitives`' {@link OperationContext} with loyalty-
501
+ * specific fields. `actorId` is narrowed to a required `string` since every
502
+ * loyalty mutation is attributed; other primitives fields (`correlationId`,
503
+ * `traceId`, `requestId`, `session`, `metadata`) flow through unchanged.
504
+ */
505
+ interface LoyaltyContext extends OperationContext {
506
+ /** Required for loyalty: who is performing this operation. Narrowed to string. */
507
+ actorId: string;
508
+ /** Narrowed from primitives' `IdLike` to string. */
509
+ organizationId?: string;
510
+ /** Override program ID for multi-program setups. */
511
+ programId?: string;
512
+ /**
513
+ * Cooperative cancellation for long-running cron loops (point expiry,
514
+ * reservation cleanup, tier evaluation, override sweeps, campaign award).
515
+ * Checked at each loop boundary via `throwIfAborted` — aborting between
516
+ * iterations never rolls back an already-committed transaction, and each
517
+ * per-item write is its own unit of work. Threaded to mongokit round-trips
518
+ * by hosts that pass it; mongokit (3.16+) honours it at every op boundary.
519
+ */
520
+ signal?: AbortSignal | undefined;
521
+ /** Dynamic tenant key support — e.g. `{ companyId: '...' }` — when a host
522
+ * configured a custom `contextKey`. */
523
+ [key: string]: unknown;
524
+ }
525
+ interface EnrollInput {
526
+ externalId: string;
527
+ externalType: string;
528
+ programId?: string;
529
+ tier?: string;
530
+ cardId?: string;
531
+ metadata?: Record<string, unknown>;
532
+ }
533
+ interface EarnPointsInput {
534
+ memberId: string;
535
+ points: number;
536
+ description: string;
537
+ referenceType?: string;
538
+ referenceId?: string;
539
+ earningRuleId?: string;
540
+ expiresAt?: Date;
541
+ idempotencyKey?: string;
542
+ metadata?: Record<string, unknown>;
543
+ }
544
+ interface AdjustPointsInput {
545
+ memberId: string;
546
+ points: number;
547
+ description: string;
548
+ reason: string;
549
+ referenceType?: string;
550
+ referenceId?: string;
551
+ idempotencyKey?: string;
552
+ metadata?: Record<string, unknown>;
553
+ }
554
+ interface CreateEarningRuleInput {
555
+ name: string;
556
+ type: 'order' | 'action' | 'category' | 'tier_bonus';
557
+ priority?: number;
558
+ description?: string;
559
+ conditions?: EarningConditions;
560
+ reward: EarningReward;
561
+ startsAt?: Date | string;
562
+ endsAt?: Date | string;
563
+ /**
564
+ * Loyalty program this rule belongs to.
565
+ *
566
+ * Optional for repository-created rules: `EarningRuleRepository.create(...)`
567
+ * and `createMany(...)` fill this from `LoyaltyShape.program.id`, matching
568
+ * the default used by `MemberRepository.enroll(...)`. Pass explicitly only
569
+ * when creating a rule for a different program.
570
+ */
571
+ programId?: string;
572
+ metadata?: Record<string, unknown>;
573
+ }
574
+ interface UpdateEarningRuleInput {
575
+ name?: string;
576
+ priority?: number;
577
+ description?: string;
578
+ conditions?: EarningConditions;
579
+ reward?: EarningReward;
580
+ startsAt?: Date | string;
581
+ endsAt?: Date | string;
582
+ metadata?: Record<string, unknown>;
583
+ }
584
+ interface CreateTierInput {
585
+ name: string;
586
+ rank: number;
587
+ qualificationCriteria?: TierQualification;
588
+ benefits?: TierBenefits;
589
+ downgrade?: TierDowngrade;
590
+ color?: string;
591
+ programId?: string;
592
+ metadata?: Record<string, unknown>;
593
+ }
594
+ interface UpdateTierInput {
595
+ name?: string;
596
+ rank?: number;
597
+ qualificationCriteria?: TierQualification;
598
+ benefits?: TierBenefits;
599
+ downgrade?: TierDowngrade;
600
+ color?: string;
601
+ metadata?: Record<string, unknown>;
602
+ }
603
+ //#endregion
604
+ export { RepositoryPlugins as A, IndexDefinition as C, LoyaltyShape as D, LoyaltyRuntime as E, StampVoucherRequest as F, TierDefinition as I, EarningRule as L, ResolvedTenant as M, TenantConfig$1 as N, ModelName as O, StampRewardIssuer as P, MemberBalance as S, LoyaltyModelName as T, RedemptionValidation as _, EnrollInput as a, PointTransaction as b, UpdateTierInput as c, EarningBreakdown as d, EarningResult as f, PaginatedResult as g, LoyaltyLiabilitySnapshot as h, EarnPointsInput as i, ResolvedConfig as j, PluginType as k, BalanceSummary as l, ListQuery as m, CreateEarningRuleInput as n, LoyaltyContext as o, ExpirationResult as p, CreateTierInput as r, UpdateEarningRuleInput as s, AdjustPointsInput as t, EarnReversalResult as u, TierEvaluation as v, LiveProgramSettings as w, Member as x, TierEvaluationBatch as y };