@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
package/dist/index.mjs CHANGED
@@ -1,11 +1,55 @@
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-DErtHQou.mjs";
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
- import { t as defineLoyaltyModels } from "./models-weWqS5Bc.mjs";
5
- import { n as createEvent, t as LoyaltyEvents } from "./loyalty-events-DXirolM_.mjs";
1
+ import "./constants-DfrENxFG.mjs";
2
+ import { A as SessionNotInTransactionError, C as MemberNotFoundError, D as ReferralNotFoundError, E as ReferralLimitExceededError, M as TierNotFoundError, N as UnmanagedSessionError, O as RuleNotFoundError, P as ValidationError, S as MemberInactiveError, T as RedemptionNotFoundError, _ as CircularReferralError, a as StampRepository, b as LoyaltyCapabilityError, c as stampProgramPatchSchema, d as RedemptionRepository, f as PointTransactionRepository, g as EarningRuleRepository, h as flushPendingLoyaltyEvents, i as StampAwardRepository, j as TenantIsolationError, k as SelfReferralError, l as stampsForOrder, m as PENDING_EVENTS, n as TierDefinitionRepository, o as assertStampProgram, p as MemberRepository, r as StampProgramRepository, s as stampProgramInputSchema, t as createRepositories, u as ReferralRepository, v as DuplicateReferralError, w as RedemptionExpiredError, x as MemberAlreadyEnrolledError, y as InsufficientPointsError } from "./repositories-ER68JmUo.mjs";
3
+ import { A as loyaltyEventDefinitions, C as StampEarned, E as TierGraceStarted, S as StampAwardIssued, T as TierDowngraded, a as MemberSuspended, b as ReferralRejected, c as PointsEarned, f as PointsRedeemed, g as RedemptionReserved, h as RedemptionReleased, i as MemberReactivated, j as InProcessLoyaltyBus, k as TierUpgraded, l as PointsExpired, m as RedemptionConfirmed, n as MemberDeactivated, p as PointsReversed, r as MemberEnrolled, s as PointsAdjusted, u as PointsExpiringSoon, v as ReferralApproved, w as StampReversed, x as ReferralRewarded, y as ReferralRecorded } from "./events-DhZolW61.mjs";
4
+ import { t as defineLoyaltyModels } from "./models-BEGLduz1.mjs";
5
+ import { isRuleInSchedule } from "./domain/rule-schedule.mjs";
6
+ import { n as createEvent, t as LoyaltyEvents } from "./loyalty-events-j29B66YW.mjs";
7
+ import { earnBase } from "./domain/earn-base.mjs";
6
8
  import { transactionResolutionOf, withTransaction } from "@classytic/mongokit";
7
9
  import { isValidTimeZone } from "@classytic/primitives/timezone";
8
10
  import { assertNoLegacyTenantKeys, resolveTenantConfig } from "@classytic/repo-core/tenant";
11
+ //#region src/domain/program-settings.ts
12
+ function assertProgram(p) {
13
+ if (!Number.isSafeInteger(p.pointValueMinor) || p.pointValueMinor <= 0) throw new ValidationError("pointValueMinor must be a positive integer (minor units per point)");
14
+ if (p.redemption.maxRedeemPercent < 0 || p.redemption.maxRedeemPercent > 100) throw new ValidationError("maxRedeemPercent must be between 0 and 100");
15
+ if (p.expiry.enabled && !(Number.isSafeInteger(p.expiry.defaultDays) && p.expiry.defaultDays >= 1)) throw new ValidationError("expiry.enabled requires expiry.defaultDays as a whole number of days >= 1");
16
+ }
17
+ /** The shape's own program settings. */
18
+ function programFromConfig(config) {
19
+ return {
20
+ pointValueMinor: config.pointValueMinor,
21
+ redemption: {
22
+ minPoints: config.redemption.minPoints,
23
+ minOrderAmount: config.redemption.minOrderAmount,
24
+ maxRedeemPercent: config.redemption.maxRedeemPercent
25
+ },
26
+ expiry: config.expiry
27
+ };
28
+ }
29
+ /** The per-operation reader: live settings over the shape's, validated every time. */
30
+ function createProgramSettings(config, live) {
31
+ const base = programFromConfig(config);
32
+ if (!live) return async () => base;
33
+ return async () => {
34
+ const l = await live();
35
+ const r = l.redemption ?? {};
36
+ const program = {
37
+ pointValueMinor: l.pointValueMinor ?? base.pointValueMinor,
38
+ redemption: {
39
+ minPoints: r.minPoints ?? base.redemption.minPoints,
40
+ minOrderAmount: r.minOrderAmount ?? base.redemption.minOrderAmount,
41
+ maxRedeemPercent: r.maxRedeemPercent ?? base.redemption.maxRedeemPercent
42
+ },
43
+ expiry: l.expiryDays === void 0 ? base.expiry : l.expiryDays === null ? { enabled: false } : {
44
+ enabled: true,
45
+ defaultDays: l.expiryDays
46
+ }
47
+ };
48
+ assertProgram(program);
49
+ return program;
50
+ };
51
+ }
52
+ //#endregion
9
53
  //#region src/index.ts
10
54
  function resolveConfig(config) {
11
55
  const tenant = resolveTenantConfig(config.tenant);
@@ -36,11 +80,14 @@ function resolveConfig(config) {
36
80
  ...config.timezone !== void 0 ? { timezone: config.timezone } : {},
37
81
  tenant
38
82
  };
83
+ assertResolvedConfig(resolved, config);
84
+ return resolved;
85
+ }
86
+ /** Fail loud at boot on a setting that would otherwise mis-evaluate silently at runtime. */
87
+ function assertResolvedConfig(resolved, config) {
39
88
  if (config.timezone !== void 0 && !isValidTimeZone(config.timezone)) throw new ValidationError(`Invalid IANA timezone: '${config.timezone}'`);
40
- if (!Number.isSafeInteger(resolved.pointValueMinor) || resolved.pointValueMinor <= 0) throw new ValidationError("pointValueMinor must be a positive integer (minor units per point)");
41
89
  if (resolved.referral.codeLength < 4) throw new ValidationError("referral codeLength must be >= 4");
42
- if (resolved.redemption.maxRedeemPercent < 0 || resolved.redemption.maxRedeemPercent > 100) throw new ValidationError("maxRedeemPercent must be between 0 and 100");
43
- return resolved;
90
+ assertProgram(programFromConfig(resolved));
44
91
  }
45
92
  var MongoUnitOfWork = class {
46
93
  connection;
@@ -120,7 +167,9 @@ function defineLoyalty(shape = {}) {
120
167
  events,
121
168
  outbox: runtime.outbox,
122
169
  notifications: runtime.notifications ?? null,
123
- unitOfWork
170
+ unitOfWork,
171
+ programSettings: createProgramSettings(resolvedConfig, runtime.programSettings),
172
+ rewardIssuer: runtime.rewardIssuer
124
173
  });
125
174
  {
126
175
  const caps = repositories.pointTransaction.capabilities;
@@ -149,4 +198,4 @@ function defineLoyalty(shape = {}) {
149
198
  };
150
199
  }
151
200
  //#endregion
152
- export { CircularReferralError, DuplicateReferralError, EarningRuleRepository, InProcessLoyaltyBus, InsufficientPointsError, LoyaltyCapabilityError, LoyaltyEvents, MemberAlreadyEnrolledError, MemberDeactivated, MemberEnrolled, MemberInactiveError, MemberNotFoundError, MemberReactivated, MemberRepository, MemberSuspended, PENDING_EVENTS, PointTransactionRepository, PointsAdjusted, PointsEarned, PointsExpired, PointsExpiringSoon, PointsRedeemed, RedemptionConfirmed, RedemptionExpiredError, RedemptionNotFoundError, RedemptionReleased, RedemptionRepository, RedemptionReserved, ReferralApproved, ReferralLimitExceededError, ReferralNotFoundError, ReferralRecorded, ReferralRejected, ReferralRepository, ReferralRewarded, RuleNotFoundError, SelfReferralError, SessionNotInTransactionError, TenantIsolationError, TierDefinitionRepository, TierDowngraded, TierGraceStarted, TierNotFoundError, TierUpgraded, UnmanagedSessionError, ValidationError, createEvent, defineLoyalty, defineLoyaltyModels, flushPendingLoyaltyEvents, loyaltyEventDefinitions, resolveConfig };
201
+ export { CircularReferralError, DuplicateReferralError, EarningRuleRepository, InProcessLoyaltyBus, InsufficientPointsError, LoyaltyCapabilityError, LoyaltyEvents, MemberAlreadyEnrolledError, MemberDeactivated, MemberEnrolled, MemberInactiveError, MemberNotFoundError, MemberReactivated, MemberRepository, MemberSuspended, PENDING_EVENTS, PointTransactionRepository, PointsAdjusted, PointsEarned, PointsExpired, PointsExpiringSoon, PointsRedeemed, PointsReversed, RedemptionConfirmed, RedemptionExpiredError, RedemptionNotFoundError, RedemptionReleased, RedemptionRepository, RedemptionReserved, ReferralApproved, ReferralLimitExceededError, ReferralNotFoundError, ReferralRecorded, ReferralRejected, ReferralRepository, ReferralRewarded, RuleNotFoundError, SelfReferralError, SessionNotInTransactionError, StampAwardIssued, StampAwardRepository, StampEarned, StampProgramRepository, StampRepository, StampReversed, TenantIsolationError, TierDefinitionRepository, TierDowngraded, TierGraceStarted, TierNotFoundError, TierUpgraded, UnmanagedSessionError, ValidationError, assertStampProgram, createEvent, defineLoyalty, defineLoyaltyModels, earnBase, flushPendingLoyaltyEvents, isRuleInSchedule, loyaltyEventDefinitions, resolveConfig, stampProgramInputSchema, stampProgramPatchSchema, stampsForOrder };
@@ -4,24 +4,25 @@ function createEvent$1(type, payload, ctx, meta) {
4
4
  const base = {};
5
5
  if (ctx?.actorId !== void 0) base.userId = ctx.actorId;
6
6
  if (ctx?.organizationId !== void 0) base.organizationId = ctx.organizationId;
7
- return createEvent(type, payload, {
7
+ return createEvent(type, toWire(payload), {
8
8
  ...base,
9
9
  ...meta
10
10
  });
11
11
  }
12
- //#endregion
13
- //#region src/events/loyalty-events.ts
14
12
  /**
15
- * Loyalty event name + payload map.
16
- *
17
- * Minimal surface. Loyalty emits events for its own lifecycle transitions
18
- * only — member, points ledger, tier, redemption, and referral.
19
- *
20
- * For registration with an arc-compatible event registry, see
21
- * `loyalty-event-catalog.ts` in this directory — it exposes each event as
22
- * a `LoyaltyEventDefinition` with a Zod payload schema + JSON Schema
23
- * derived via `z.toJSONSchema()` (PACKAGE_RULES §18.5).
13
+ * The catalog declares every id a STRING. A document's `_id` is an ObjectId in process, so it
14
+ * is converted here — the one seam every loyalty event passes — or an in-process subscriber
15
+ * sees a different shape than the outbox delivers, and a strict consumer refuses it.
24
16
  */
17
+ function toWire(value) {
18
+ if (value === null || typeof value !== "object") return value;
19
+ if (value._bsontype === "ObjectId") return String(value);
20
+ if (value instanceof Date) return value;
21
+ if (Array.isArray(value)) return value.map(toWire);
22
+ return Object.fromEntries(Object.entries(value).map(([k, v]) => [k, toWire(v)]));
23
+ }
24
+ //#endregion
25
+ //#region src/events/loyalty-events.ts
25
26
  const LoyaltyEvents = {
26
27
  MEMBER_ENROLLED: "loyalty.member.enrolled",
27
28
  MEMBER_DEACTIVATED: "loyalty.member.deactivated",
@@ -30,7 +31,10 @@ const LoyaltyEvents = {
30
31
  POINTS_EARNED: "loyalty.points.earned",
31
32
  POINTS_REDEEMED: "loyalty.points.redeemed",
32
33
  POINTS_ADJUSTED: "loyalty.points.adjusted",
34
+ POINTS_REVERSED: "loyalty.points.reversed",
33
35
  POINTS_EXPIRED: "loyalty.points.expired",
36
+ /** One expired lot, dispatched atomically with its expiry — the per-fact companion of POINTS_EXPIRED. */
37
+ POINTS_LOT_EXPIRED: "loyalty.points.lot_expired",
34
38
  POINTS_EXPIRING_SOON: "loyalty.points.expiring_soon",
35
39
  TIER_UPGRADED: "loyalty.tier.upgraded",
36
40
  TIER_DOWNGRADED: "loyalty.tier.downgraded",
@@ -46,7 +50,10 @@ const LoyaltyEvents = {
46
50
  REFERRAL_APPROVED: "loyalty.referral.approved",
47
51
  REFERRAL_REJECTED: "loyalty.referral.rejected",
48
52
  REFERRAL_REWARDED: "loyalty.referral.rewarded",
49
- MULTI_TIER_REFERRAL_AWARDED: "loyalty.referral.multi_tier_awarded"
53
+ MULTI_TIER_REFERRAL_AWARDED: "loyalty.referral.multi_tier_awarded",
54
+ STAMP_EARNED: "loyalty.stamp.earned",
55
+ STAMP_REVERSED: "loyalty.stamp.reversed",
56
+ STAMP_AWARD_ISSUED: "loyalty.stamp.award_issued"
50
57
  };
51
58
  //#endregion
52
59
  export { createEvent$1 as n, LoyaltyEvents as t };
@@ -1,3 +1,3 @@
1
- import { n as LoyaltyModelName } from "../config-CtkGj3xZ.mjs";
2
- import { n as LoyaltyModels, r as defineLoyaltyModels, t as DefineLoyaltyModelsConfig } from "../index-BIhVuwIZ.mjs";
1
+ import { T as LoyaltyModelName } from "../index-DRrEhE0r.mjs";
2
+ import { n as LoyaltyModels, r as defineLoyaltyModels, t as DefineLoyaltyModelsConfig } from "../index-CorB0JL6.mjs";
3
3
  export { DefineLoyaltyModelsConfig, type LoyaltyModelName, LoyaltyModels, defineLoyaltyModels };
@@ -1,2 +1 @@
1
- import { t as defineLoyaltyModels } from "../models-weWqS5Bc.mjs";
2
- export { defineLoyaltyModels };
1
+ export { t as defineLoyaltyModels } from "../models-BEGLduz1.mjs";
@@ -1,4 +1,4 @@
1
- import { a as REFERRAL_STATUSES, c as TRANSACTION_TYPES, i as REDEMPTION_STATUSES, o as RULE_STATUSES, r as MEMBER_STATUSES, s as RULE_TYPES } from "./constants-DOPBh6kt.mjs";
1
+ import { a as REDEMPTION_STATUSES, c as RULE_TYPES, d as STAMP_PROGRAM_STATUSES, f as STAMP_REWARD_KINDS, i as MEMBER_STATUSES, l as STAMP_AWARD_STATUSES, o as REFERRAL_STATUSES, p as TRANSACTION_TYPES, r as EARN_SOURCES, s as RULE_STATUSES, u as STAMP_EARN_UNITS } from "./constants-DfrENxFG.mjs";
2
2
  import { defineModels } from "@classytic/mongokit";
3
3
  import mongoose from "mongoose";
4
4
  import { mongooseMinorUnits } from "@classytic/validation/money";
@@ -212,12 +212,14 @@ function createMemberSchema() {
212
212
  externalType: 1
213
213
  }, { unique: true });
214
214
  schema.index({ cardId: 1 }, {
215
+ name: "cardId_str",
215
216
  unique: true,
216
- partialFilterExpression: { cardId: { $type: "string" } }
217
+ partialFilterExpression: { cardId: { $gte: "" } }
217
218
  });
218
219
  schema.index({ referralCode: 1 }, {
220
+ name: "referralCode_str",
219
221
  unique: true,
220
- partialFilterExpression: { referralCode: { $type: "string" } }
222
+ partialFilterExpression: { referralCode: { $gte: "" } }
221
223
  });
222
224
  return schema;
223
225
  }
@@ -251,6 +253,16 @@ function createPointTransactionSchema() {
251
253
  type: String,
252
254
  required: true
253
255
  },
256
+ /** Earn rows: why the points were granted (closed set). */
257
+ source: {
258
+ type: String,
259
+ enum: EARN_SOURCES
260
+ },
261
+ /** Minor units one point was worth when this row was written (the programme rate then). */
262
+ pointValueMinor: {
263
+ type: Number,
264
+ min: 1
265
+ },
254
266
  referenceType: { type: String },
255
267
  referenceId: { type: String },
256
268
  idempotencyKey: { type: String },
@@ -282,9 +294,18 @@ function createPointTransactionSchema() {
282
294
  memberId: 1,
283
295
  createdAt: -1
284
296
  });
297
+ schema.index({
298
+ expiredAt: 1,
299
+ expiresAt: 1,
300
+ _id: 1
301
+ }, {
302
+ name: "expiry_sweep",
303
+ partialFilterExpression: { expiresAt: { $exists: true } }
304
+ });
285
305
  schema.index({ idempotencyKey: 1 }, {
306
+ name: "idempotencyKey_str",
286
307
  unique: true,
287
- partialFilterExpression: { idempotencyKey: { $type: "string" } }
308
+ partialFilterExpression: { idempotencyKey: { $gte: "" } }
288
309
  });
289
310
  return schema;
290
311
  }
@@ -421,6 +442,224 @@ function createReferralSchema() {
421
442
  return schema;
422
443
  }
423
444
  //#endregion
445
+ //#region src/models/schemas/stamp-award.schema.ts
446
+ function createStampAwardSchema() {
447
+ const schema = new mongoose.Schema({
448
+ stampProgramId: {
449
+ type: mongoose.Schema.Types.ObjectId,
450
+ required: true,
451
+ ref: "LoyaltyStampProgram"
452
+ },
453
+ memberId: {
454
+ type: mongoose.Schema.Types.ObjectId,
455
+ required: true,
456
+ ref: "LoyaltyMember"
457
+ },
458
+ sequence: {
459
+ type: Number,
460
+ required: true,
461
+ min: 1
462
+ },
463
+ awardKey: {
464
+ type: String,
465
+ required: true
466
+ },
467
+ entries: [{
468
+ _id: false,
469
+ entryId: {
470
+ type: mongoose.Schema.Types.ObjectId,
471
+ required: true
472
+ },
473
+ stamps: {
474
+ type: Number,
475
+ required: true,
476
+ min: 1
477
+ }
478
+ }],
479
+ stampsConsumed: {
480
+ type: Number,
481
+ required: true,
482
+ min: 1
483
+ },
484
+ status: {
485
+ type: String,
486
+ enum: STAMP_AWARD_STATUSES,
487
+ required: true
488
+ },
489
+ reward: {
490
+ kind: {
491
+ type: String,
492
+ enum: STAMP_REWARD_KINDS,
493
+ required: true
494
+ },
495
+ points: { type: Number },
496
+ pointTransactionId: { type: mongoose.Schema.Types.ObjectId },
497
+ voucherProgramId: { type: String },
498
+ voucherId: { type: String },
499
+ code: { type: String }
500
+ },
501
+ awardedAt: {
502
+ type: Date,
503
+ required: true
504
+ },
505
+ issuedAt: { type: Date }
506
+ }, { timestamps: true });
507
+ schema.index({
508
+ stampProgramId: 1,
509
+ memberId: 1,
510
+ sequence: 1
511
+ }, {
512
+ name: "stamp_award_sequence",
513
+ unique: true
514
+ });
515
+ schema.index({ awardKey: 1 }, {
516
+ name: "stamp_award_key",
517
+ unique: true
518
+ });
519
+ return schema;
520
+ }
521
+ //#endregion
522
+ //#region src/models/schemas/stamp-entry.schema.ts
523
+ function createStampEntrySchema() {
524
+ const schema = new mongoose.Schema({
525
+ stampProgramId: {
526
+ type: mongoose.Schema.Types.ObjectId,
527
+ required: true,
528
+ ref: "LoyaltyStampProgram"
529
+ },
530
+ memberId: {
531
+ type: mongoose.Schema.Types.ObjectId,
532
+ required: true,
533
+ ref: "LoyaltyMember"
534
+ },
535
+ orderRef: {
536
+ type: String,
537
+ required: true
538
+ },
539
+ stamps: {
540
+ type: Number,
541
+ required: true,
542
+ min: 1
543
+ },
544
+ consumed: {
545
+ type: Number,
546
+ default: 0,
547
+ min: 0
548
+ },
549
+ earnedAt: {
550
+ type: Date,
551
+ required: true
552
+ },
553
+ expiresAt: { type: Date },
554
+ businessDay: {
555
+ type: String,
556
+ required: true
557
+ },
558
+ visitKey: { type: String },
559
+ branchId: { type: String },
560
+ reversedAt: { type: Date },
561
+ reversedStamps: {
562
+ type: Number,
563
+ min: 0
564
+ }
565
+ }, { timestamps: true });
566
+ schema.index({
567
+ stampProgramId: 1,
568
+ memberId: 1,
569
+ orderRef: 1
570
+ }, {
571
+ name: "stamp_order",
572
+ unique: true
573
+ });
574
+ schema.index({
575
+ stampProgramId: 1,
576
+ memberId: 1,
577
+ visitKey: 1
578
+ }, {
579
+ name: "stamp_visit_day",
580
+ unique: true,
581
+ partialFilterExpression: { visitKey: { $gte: "" } }
582
+ });
583
+ schema.index({ orderRef: 1 });
584
+ return schema;
585
+ }
586
+ //#endregion
587
+ //#region src/models/schemas/stamp-program.schema.ts
588
+ /** Allow-lists default to ABSENT ("any"), never `[]` — an empty list is refused at write. */
589
+ const allowList = {
590
+ type: [String],
591
+ default: void 0
592
+ };
593
+ function createStampProgramSchema() {
594
+ const schema = new mongoose.Schema({
595
+ programId: {
596
+ type: String,
597
+ default: "default"
598
+ },
599
+ name: {
600
+ type: String,
601
+ required: true
602
+ },
603
+ description: { type: String },
604
+ status: {
605
+ type: String,
606
+ enum: STAMP_PROGRAM_STATUSES,
607
+ default: "active"
608
+ },
609
+ earn: {
610
+ unit: {
611
+ type: String,
612
+ enum: STAMP_EARN_UNITS,
613
+ required: true
614
+ },
615
+ categoryIds: allowList,
616
+ productIds: allowList,
617
+ perMinor: {
618
+ type: Number,
619
+ min: 1
620
+ },
621
+ minOrderMinor: {
622
+ type: Number,
623
+ min: 0
624
+ }
625
+ },
626
+ stampsRequired: {
627
+ type: Number,
628
+ required: true,
629
+ min: 1
630
+ },
631
+ stampValidityDays: {
632
+ type: Number,
633
+ min: 1
634
+ },
635
+ startsAt: { type: Date },
636
+ endsAt: { type: Date },
637
+ reward: {
638
+ kind: {
639
+ type: String,
640
+ enum: STAMP_REWARD_KINDS,
641
+ required: true
642
+ },
643
+ points: {
644
+ type: Number,
645
+ min: 1
646
+ },
647
+ voucherProgramId: { type: String }
648
+ },
649
+ maxRewardsPerMember: {
650
+ type: Number,
651
+ min: 1
652
+ },
653
+ branchIds: allowList,
654
+ metadata: { type: mongoose.Schema.Types.Mixed }
655
+ }, { timestamps: true });
656
+ schema.index({
657
+ programId: 1,
658
+ status: 1
659
+ });
660
+ return schema;
661
+ }
662
+ //#endregion
424
663
  //#region src/models/schemas/tier-definition.schema.ts
425
664
  function createTierDefinitionSchema() {
426
665
  const schema = new mongoose.Schema({
@@ -486,7 +725,10 @@ const DEFAULT_COLLECTIONS = {
486
725
  EarningRule: "loyalty_earning_rules",
487
726
  TierDefinition: "loyalty_tier_definitions",
488
727
  Redemption: "loyalty_redemptions",
489
- Referral: "loyalty_referrals"
728
+ Referral: "loyalty_referrals",
729
+ StampProgram: "loyalty_stamp_programs",
730
+ StampEntry: "loyalty_stamp_entries",
731
+ StampAward: "loyalty_stamp_awards"
490
732
  };
491
733
  /** The registered Mongoose model names — stable, independent of `collectionPrefix`. */
492
734
  const MODEL_NAMES = {
@@ -495,7 +737,10 @@ const MODEL_NAMES = {
495
737
  EarningRule: "LoyaltyEarningRule",
496
738
  TierDefinition: "LoyaltyTierDefinition",
497
739
  Redemption: "LoyaltyRedemption",
498
- Referral: "LoyaltyReferral"
740
+ Referral: "LoyaltyReferral",
741
+ StampProgram: "LoyaltyStampProgram",
742
+ StampEntry: "LoyaltyStampEntry",
743
+ StampAward: "LoyaltyStampAward"
499
744
  };
500
745
  const ENTITIES = [
501
746
  {
@@ -539,6 +784,27 @@ const ENTITIES = [
539
784
  collection: "Referral",
540
785
  indexKey: "referral",
541
786
  factory: createReferralSchema
787
+ },
788
+ {
789
+ key: "StampProgram",
790
+ modelName: MODEL_NAMES.StampProgram,
791
+ collection: "StampProgram",
792
+ indexKey: "stampProgram",
793
+ factory: createStampProgramSchema
794
+ },
795
+ {
796
+ key: "StampEntry",
797
+ modelName: MODEL_NAMES.StampEntry,
798
+ collection: "StampEntry",
799
+ indexKey: "stampEntry",
800
+ factory: createStampEntrySchema
801
+ },
802
+ {
803
+ key: "StampAward",
804
+ modelName: MODEL_NAMES.StampAward,
805
+ collection: "StampAward",
806
+ indexKey: "stampAward",
807
+ factory: createStampAwardSchema
542
808
  }
543
809
  ];
544
810
  /**
@@ -581,7 +847,10 @@ function defineLoyaltyModels(config) {
581
847
  EarningRule: models.get(MODEL_NAMES.EarningRule),
582
848
  TierDefinition: models.get(MODEL_NAMES.TierDefinition),
583
849
  Redemption: models.get(MODEL_NAMES.Redemption),
584
- Referral: models.get(MODEL_NAMES.Referral)
850
+ Referral: models.get(MODEL_NAMES.Referral),
851
+ StampProgram: models.get(MODEL_NAMES.StampProgram),
852
+ StampEntry: models.get(MODEL_NAMES.StampEntry),
853
+ StampAward: models.get(MODEL_NAMES.StampAward)
585
854
  })
586
855
  });
587
856
  }
@@ -1,6 +1,6 @@
1
1
  import { t as NotificationAdapter } from "../notification.port-wY4IKimz.mjs";
2
2
  //#region src/notifications/index.d.ts
3
- interface NotificationsAdapterConfig {
3
+ export interface NotificationsAdapterConfig {
4
4
  service: {
5
5
  send: (payload: unknown) => Promise<unknown>;
6
6
  };
@@ -15,6 +15,5 @@ interface NotificationsAdapterConfig {
15
15
  channels?: Record<string, string[]>;
16
16
  skip?: string[];
17
17
  }
18
- declare function createNotificationsAdapter(config: NotificationsAdapterConfig): NotificationAdapter;
19
- //#endregion
20
- export { NotificationsAdapterConfig, createNotificationsAdapter };
18
+ export declare function createNotificationsAdapter(config: NotificationsAdapterConfig): NotificationAdapter;
19
+ //#endregion
@@ -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-BIPHUsfR.mjs";
2
- export { CreateRepositoriesInput, EarningRuleRepository, LoyaltyRepositories, MemberRepository, PointTransactionRepository, RedemptionRepository, ReferralRepository, TierDefinitionRepository, createRepositories };
1
+ import { _ as PointTransactionRepository, a as StampProgramRepository, g as EarningRuleRepository, i as StampRepository, l as RedemptionRepository, n as LoyaltyRepositories, o as StampAwardRepository, r as createRepositories, s as ReferralRepository, t as CreateRepositoriesInput, v as MemberRepository, y as TierDefinitionRepository } from "../index-BvyzKU7R.mjs";
2
+ export { CreateRepositoriesInput, EarningRuleRepository, LoyaltyRepositories, MemberRepository, PointTransactionRepository, RedemptionRepository, ReferralRepository, StampAwardRepository, StampProgramRepository, StampRepository, TierDefinitionRepository, createRepositories };
@@ -1,2 +1 @@
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
- export { EarningRuleRepository, MemberRepository, PointTransactionRepository, RedemptionRepository, ReferralRepository, TierDefinitionRepository, createRepositories };
1
+ export { g as EarningRuleRepository, p as MemberRepository, f as PointTransactionRepository, d as RedemptionRepository, u as ReferralRepository, i as StampAwardRepository, r as StampProgramRepository, a as StampRepository, n as TierDefinitionRepository, t as createRepositories } from "../repositories-ER68JmUo.mjs";