@classytic/loyalty 0.4.0 → 0.6.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.
@@ -1,5 +1,5 @@
1
- import { n as createEvent, t as LoyaltyEvents } from "./loyalty-events-C38JA3cU.mjs";
2
- import { Repository, multiTenantPlugin } from "@classytic/mongokit";
1
+ import { n as createEvent, t as LoyaltyEvents } from "./loyalty-events-DXirolM_.mjs";
2
+ import { Repository, appendOnlyPlugin, multiTenantPlugin } from "@classytic/mongokit";
3
3
  import { localTimeParts } from "@classytic/primitives/timezone";
4
4
  import { addDays, isoWeekday } from "@classytic/primitives/calendar";
5
5
  import { randomBytes } from "node:crypto";
@@ -152,6 +152,27 @@ var UnmanagedSessionError = class extends LoyaltyError {
152
152
  }
153
153
  };
154
154
  /**
155
+ * A host passed `ctx.session`, but that session has no ACTIVE transaction.
156
+ *
157
+ * A bare `startSession()` provides NO atomicity — every write inside it
158
+ * commits independently. Accepting it as proof of a unit of work is the
159
+ * silent-permissiveness failure in its purest form: loyalty then saves the
160
+ * outbox row "in-session" and applies the balance `$inc` "in-session", and
161
+ * if the outbox save fails the balance mutation still stands with no ledger
162
+ * row behind it. Nothing throws; the points are simply wrong.
163
+ *
164
+ * `session.inTransaction()` is the driver's own answer, so this check cannot
165
+ * drift from what MongoDB actually thinks. `@classytic/access` refuses
166
+ * exactly this at `entitlement.repository.ts` — same rule, same reason.
167
+ */
168
+ var SessionNotInTransactionError = class extends LoyaltyError {
169
+ code = "loyalty.transaction.session_not_in_transaction";
170
+ status = 409;
171
+ constructor() {
172
+ super("[loyalty] ctx.session was supplied but has no ACTIVE transaction — a bare session provides no atomicity (each write commits independently), so a failing outbox.save would leave the points mutation standing with the event lost. Drive it with session.withTransaction(...) / startTransaction(), or omit ctx.session and let loyalty open its own transaction.");
173
+ }
174
+ };
175
+ /**
155
176
  * The backend doesn't declare the runtime capabilities loyalty's
156
177
  * transactional verbs require (earn / adjust / reserve / confirm / release /
157
178
  * reverse all run inside `withTransaction`). MongoDB must run as a replica set
@@ -161,8 +182,18 @@ var UnmanagedSessionError = class extends LoyaltyError {
161
182
  var LoyaltyCapabilityError = class extends LoyaltyError {
162
183
  code = "loyalty.engine.missing_capabilities";
163
184
  status = 500;
164
- constructor(missing) {
165
- super(`[loyalty] backend missing required capabilities: ${missing.join(", ")}. Point earn / redemption / reversal paths are transactional — MongoDB must run as a replica set or sharded cluster.`);
185
+ /**
186
+ * `true` when the backend never ANSWERED (bound before the connection
187
+ * opened, a driver exposing no topology) rather than answering "no". Both
188
+ * refuse — an unobserved outcome is never a positive one (AGENTS.md FAIL
189
+ * LOUD rule 3) — but the operator's fix differs, so the message must.
190
+ */
191
+ indeterminate;
192
+ constructor(missing, opts = {}) {
193
+ const indeterminate = opts.indeterminate === true;
194
+ const backendMissing = missing.filter((flag) => !flag.startsWith("outbox."));
195
+ super(indeterminate ? `[loyalty] could NOT determine whether the backend supports: ${missing.join(", ")}. An unconfirmed answer is treated as unsupported — point earn / redemption / reversal paths are transactional. Bind AFTER \`await mongoose.connect(...)\`, or call \`await probeMongoCapabilities(connection)\` first, or set \`allowNonTransactional: true\`.` : `[loyalty] missing required capabilities: ${missing.join(", ")}.` + (backendMissing.length > 0 ? " Point earn / redemption / reversal paths are transactional — MongoDB must run as a replica set or sharded cluster, or set `allowNonTransactional: true`." : ""));
196
+ this.indeterminate = indeterminate;
166
197
  }
167
198
  };
168
199
  //#endregion
@@ -234,8 +265,9 @@ function validatePositivePoints(points, label = "points") {
234
265
  validatePoints(points, label);
235
266
  if (points <= 0) throw new ValidationError(`${label} must be positive`);
236
267
  }
268
+ /** `orderTotal` is an integer in the host's minor currency unit (paisa for BDT). */
237
269
  function validateOrderTotal(total) {
238
- if (!Number.isFinite(total) || total < 0) throw new ValidationError("orderTotal must be a non-negative finite number");
270
+ if (!Number.isSafeInteger(total) || total < 0) throw new ValidationError("orderTotal must be a non-negative safe integer (minor units)");
239
271
  }
240
272
  //#endregion
241
273
  //#region src/repositories/earning-rule.repository.ts
@@ -251,7 +283,7 @@ var EarningRuleRepository = class extends Repository {
251
283
  injectTenantIntoData(data, options, this.config.tenant);
252
284
  }
253
285
  /**
254
- * Auto-injects `programId` from `LoyaltyConfig.program.id` and the tenant id
286
+ * Auto-injects `programId` from `LoyaltyShape.program.id` and the tenant id
255
287
  * from repo options when omitted. This keeps custom-program engines aligned
256
288
  * with `MemberRepository.enroll(...)`, which uses the same config default.
257
289
  */
@@ -264,6 +296,14 @@ var EarningRuleRepository = class extends Repository {
264
296
  for (const doc of docs) this.applyCreateDefaults(doc, opts);
265
297
  return super.createMany(docs, options);
266
298
  }
299
+ /**
300
+ * Evaluates every active order/category/tier_bonus rule against an order.
301
+ *
302
+ * `orderTotal` and `items[].amount` are **integers in the host's minor
303
+ * currency unit** (paisa for BDT) — `50000` = ৳500. Rates on the rule
304
+ * (`amountPerPoint`, `pointsPerAmount` + `rateBasis`) are quoted in the
305
+ * same minor units.
306
+ */
267
307
  async evaluateOrder(input, ctx) {
268
308
  validateOrderTotal(input.orderTotal);
269
309
  assertTenantContext(ctx, this.config.tenant);
@@ -394,18 +434,34 @@ var EarningRuleRepository = class extends Repository {
394
434
  weekday(instant) {
395
435
  return (this.config.timezone ? localTimeParts(instant, this.config.timezone).isoWeekday : isoWeekday(instant)) % 7;
396
436
  }
437
+ /**
438
+ * `orderTotal` is integer minor units. Two rate shapes:
439
+ * - `pointsPerAmount` + `rateBasis`: `points = pointsPerAmount × (orderTotal / rateBasis)`
440
+ * - `amountPerPoint` (minor units per point): `points = orderTotal / amountPerPoint`
441
+ * Rounding is applied ONCE at the end, per the rule's `roundingMode`.
442
+ */
397
443
  calculateOrderPoints(rule, orderTotal) {
398
444
  const { reward } = rule;
399
445
  let points = 0;
400
- if (reward.pointsPerAmount) points = orderTotal * reward.pointsPerAmount;
446
+ if (reward.pointsPerAmount) points = reward.pointsPerAmount * (orderTotal / this.requireRateBasis(rule));
401
447
  else if (reward.amountPerPoint && reward.amountPerPoint > 0) points = orderTotal / reward.amountPerPoint;
402
448
  return this.applyRounding(points, reward.roundingMode);
403
449
  }
450
+ /**
451
+ * `pointsPerAmount` has NO implicit basis — a rule that carries it without
452
+ * an explicit integer minor-unit `rateBasis` is malformed (the schema
453
+ * enforces the pairing on write; this guards legacy/hand-written docs).
454
+ */
455
+ requireRateBasis(rule) {
456
+ const rateBasis = rule.reward.rateBasis;
457
+ if (typeof rateBasis !== "number" || !Number.isInteger(rateBasis) || rateBasis < 1) throw new ValidationError(`Earning rule '${rule.name}' (${rule._id}) sets reward.pointsPerAmount without a valid integer reward.rateBasis (minor units) — points = pointsPerAmount × (orderTotal / rateBasis)`);
458
+ return rateBasis;
459
+ }
404
460
  calculateCategoryPoints(rule, items) {
405
461
  const { conditions, reward } = rule;
406
462
  let points = 0;
407
463
  for (const item of items) if (conditions.categories && item.categoryId && conditions.categories.includes(item.categoryId)) {
408
- if (reward.pointsPerAmount) points += item.amount * reward.pointsPerAmount;
464
+ if (reward.pointsPerAmount) points += reward.pointsPerAmount * (item.amount / this.requireRateBasis(rule));
409
465
  else if (reward.amountPerPoint) points += item.amount / reward.amountPerPoint;
410
466
  }
411
467
  return this.applyRounding(points, reward.roundingMode);
@@ -428,7 +484,24 @@ var EarningRuleRepository = class extends Repository {
428
484
  * host-supplied context fields and never appears in the public type.
429
485
  */
430
486
  const PENDING_EVENTS = Symbol.for("@classytic/loyalty:pendingEvents");
487
+ /**
488
+ * A caller-supplied `ctx.session` is only evidence of a unit of work if it
489
+ * has an ACTIVE transaction. A bare `startSession()` commits every write
490
+ * independently, so treating it as atomic is how a balance `$inc` lands with
491
+ * no ledger row behind it when the outbox save fails — silently, because the
492
+ * business write already succeeded.
493
+ *
494
+ * `inTransaction()` is the driver's own answer, so this cannot drift from
495
+ * what Mongo actually thinks. Same guard, same rationale as
496
+ * `@classytic/access`'s `atomically()`.
497
+ */
498
+ function assertSessionInTransaction(ctx) {
499
+ if (!ctx.session) return;
500
+ const session = ctx.session;
501
+ if (typeof session.inTransaction !== "function" || !session.inTransaction()) throw new SessionNotInTransactionError();
502
+ }
431
503
  async function dispatchLoyaltyEvent(deps, event, ctx) {
504
+ assertSessionInTransaction(ctx);
432
505
  const pending = ctx[PENDING_EVENTS];
433
506
  if (deps.outbox) try {
434
507
  const saveOptions = {};
@@ -469,7 +542,10 @@ async function dispatchLoyaltyEvent(deps, event, ctx) {
469
542
  */
470
543
  async function runLoyaltyUnit(unitOfWork, deps, ctx, body) {
471
544
  const internalCtx = ctx;
472
- if (ctx.session) return body(internalCtx);
545
+ if (ctx.session) {
546
+ assertSessionInTransaction(ctx);
547
+ return body(internalCtx);
548
+ }
473
549
  const pending = [];
474
550
  const result = await unitOfWork.withTransaction(async (session) => {
475
551
  pending.length = 0;
@@ -709,7 +785,7 @@ var PointTransactionRepository = class extends Repository {
709
785
  unitOfWork;
710
786
  config;
711
787
  constructor(model, plugins = []) {
712
- super(model, plugins);
788
+ super(model, [...plugins, appendOnlyPlugin()]);
713
789
  }
714
790
  /** Session-bound outbox save + transport publish (PACKAGE_RULES §5.5 + P8). */
715
791
  async _dispatch(event, ctx) {
@@ -984,13 +1060,33 @@ var PointTransactionRepository = class extends Repository {
984
1060
  const affectedMemberIds = /* @__PURE__ */ new Set();
985
1061
  for (const tx of expiredTxs) {
986
1062
  throwIfAborted(ctx.signal);
987
- await this.unitOfWork.withTransaction(async (session) => {
1063
+ const expiredAmount = await this.unitOfWork.withTransaction(async (session) => {
1064
+ const member = await this.memberRepo.requireMember(tx.memberId, {
1065
+ session,
1066
+ ...tc
1067
+ });
1068
+ const available = Math.max(0, member.balance.current);
1069
+ const amount = Math.min(tx.points, available);
1070
+ await this.update(tx._id, { expiredAt: now }, {
1071
+ session,
1072
+ ...tc,
1073
+ bypassAppendOnly: true
1074
+ });
1075
+ if (amount <= 0) return 0;
1076
+ const updatedMember = await this.memberRepo.updateBalance(tx.memberId, {
1077
+ current: -amount,
1078
+ expired: amount
1079
+ }, {
1080
+ session,
1081
+ ...tc
1082
+ });
1083
+ if (updatedMember.balance.current < 0) throw new InsufficientPointsError(updatedMember.balance.current + amount, amount);
988
1084
  await this.create({
989
1085
  memberId: tx.memberId,
990
1086
  programId: tx.programId,
991
1087
  type: "expire",
992
- points: -tx.points,
993
- balanceAfter: 0,
1088
+ points: -amount,
1089
+ balanceAfter: updatedMember.balance.current,
994
1090
  description: `Points expired from transaction ${tx._id}`,
995
1091
  referenceType: "expiration",
996
1092
  referenceId: tx._id,
@@ -999,27 +1095,21 @@ var PointTransactionRepository = class extends Repository {
999
1095
  session,
1000
1096
  ...tc
1001
1097
  });
1002
- await this.update(tx._id, { expiredAt: now }, {
1003
- session,
1004
- ...tc
1005
- });
1006
- await this.memberRepo.updateBalance(tx.memberId, {
1007
- current: -tx.points,
1008
- expired: tx.points
1009
- }, {
1010
- session,
1011
- ...tc
1012
- });
1098
+ return amount;
1013
1099
  });
1014
- affectedMemberIds.add(tx.memberId);
1015
- totalExpired += tx.points;
1016
- transactionCount++;
1100
+ if (expiredAmount > 0) {
1101
+ affectedMemberIds.add(tx.memberId);
1102
+ totalExpired += expiredAmount;
1103
+ transactionCount++;
1104
+ }
1017
1105
  }
1018
- if (totalExpired > 0) await this._dispatch(createEvent(LoyaltyEvents.POINTS_EXPIRED, {
1019
- membersAffected: affectedMemberIds.size,
1020
- pointsExpired: totalExpired,
1021
- transactionCount
1022
- }, ctx), ctx);
1106
+ if (totalExpired > 0) await runLoyaltyUnit(this.unitOfWork, this._dispatchDeps, ctx, async (txCtx) => {
1107
+ await this._dispatch(createEvent(LoyaltyEvents.POINTS_EXPIRED, {
1108
+ membersAffected: affectedMemberIds.size,
1109
+ pointsExpired: totalExpired,
1110
+ transactionCount
1111
+ }, txCtx), txCtx);
1112
+ });
1023
1113
  return {
1024
1114
  membersAffected: affectedMemberIds.size,
1025
1115
  pointsExpired: totalExpired,
@@ -1090,20 +1180,36 @@ var RedemptionRepository = class extends Repository {
1090
1180
  }
1091
1181
  /**
1092
1182
  * Atomic compare-and-set status transition.
1093
- * Returns null if the current status doesn't match `expectedStatus` (CAS mismatch).
1183
+ *
1184
+ * `null` means ONE thing and one thing only: the guarded update matched
1185
+ * no document, i.e. the status was not `expectedStatus`. `throwOnNotFound:
1186
+ * false` already produces that `null`, so there is nothing legitimate left
1187
+ * for a `catch` to convert.
1188
+ *
1189
+ * There used to be a bare `} catch { return null; }` here. It laundered
1190
+ * every real failure into a CAS miss, and callers map a CAS miss to
1191
+ * `ValidationError("Redemption status must be 'reserved', got '<status>'")`
1192
+ * — a NON-retryable 4xx built from the status read BEFORE the write. So a
1193
+ * routine Mongo `WriteConflict` (a retryable transient, the normal outcome
1194
+ * of two concurrent confirms) surfaced as a client error whose message
1195
+ * could literally read "must be 'reserved', got 'reserved'": an operator
1196
+ * chasing a contradiction while the actual fix was to retry.
1197
+ *
1198
+ * Anything that throws in here is a real error and must propagate.
1094
1199
  */
1095
1200
  async atomicStatusTransition(id, expectedStatus, data, ctx) {
1096
- try {
1097
- return await this.update(id, data, {
1098
- lean: true,
1099
- query: { status: expectedStatus },
1100
- throwOnNotFound: false,
1101
- ...ctx
1102
- });
1103
- } catch {
1104
- return null;
1105
- }
1201
+ return this.update(id, data, {
1202
+ lean: true,
1203
+ query: { status: expectedStatus },
1204
+ throwOnNotFound: false,
1205
+ ...ctx
1206
+ });
1106
1207
  }
1208
+ /**
1209
+ * Validates a redemption request. `orderTotal` is an **integer in the
1210
+ * host's minor currency unit** (paisa for BDT); the returned
1211
+ * `discountAmount` is in the same minor units.
1212
+ */
1107
1213
  async validate(input, ctx) {
1108
1214
  validatePositivePoints(input.pointsToRedeem, "pointsToRedeem");
1109
1215
  validateOrderTotal(input.orderTotal);
@@ -1112,7 +1218,7 @@ var RedemptionRepository = class extends Repository {
1112
1218
  const tc = tenantCtx(this.config.tenant, tenantId);
1113
1219
  const member = await this.memberRepo.requireActiveMember(input.memberId, tc);
1114
1220
  const { minPoints, minOrderAmount, maxRedeemPercent } = this.config.redemption;
1115
- const { conversionRate } = this.config;
1221
+ const { pointValueMinor } = this.config;
1116
1222
  if (input.pointsToRedeem < minPoints) return {
1117
1223
  valid: false,
1118
1224
  error: `Minimum ${minPoints} points required`,
@@ -1134,17 +1240,24 @@ var RedemptionRepository = class extends Repository {
1134
1240
  discountAmount: 0,
1135
1241
  maxAllowedPoints: member.balance.current
1136
1242
  };
1137
- const maxDiscount = Math.floor(input.orderTotal * maxRedeemPercent / 100);
1138
- const requestedDiscount = Math.floor(input.pointsToRedeem / conversionRate);
1139
- const discountAmount = Math.min(requestedDiscount, maxDiscount);
1140
- const maxAllowedPoints = Math.floor(maxDiscount * conversionRate);
1243
+ const maxDiscountMinor = Math.floor(input.orderTotal * maxRedeemPercent / 100);
1244
+ const requestedDiscountMinor = input.pointsToRedeem * pointValueMinor;
1245
+ const cappedDiscountMinor = Math.min(requestedDiscountMinor, maxDiscountMinor);
1246
+ const maxAllowedPoints = Math.floor(maxDiscountMinor / pointValueMinor);
1247
+ const actualPoints = cappedDiscountMinor < requestedDiscountMinor ? Math.floor(cappedDiscountMinor / pointValueMinor) : input.pointsToRedeem;
1141
1248
  return {
1142
1249
  valid: true,
1143
- pointsToRedeem: discountAmount < requestedDiscount ? Math.floor(discountAmount * conversionRate) : input.pointsToRedeem,
1144
- discountAmount,
1250
+ pointsToRedeem: actualPoints,
1251
+ discountAmount: actualPoints * pointValueMinor,
1145
1252
  maxAllowedPoints
1146
1253
  };
1147
1254
  }
1255
+ /**
1256
+ * Reserves points against an order. `orderTotal` is an **integer in the
1257
+ * host's minor currency unit** (paisa for BDT). The persisted
1258
+ * `discountAmount` is minor units and the reserve-time `pointValueMinor`
1259
+ * rate is snapshotted on the doc.
1260
+ */
1148
1261
  async reserve(input, ctx) {
1149
1262
  assertTenantContext(ctx, this.config.tenant);
1150
1263
  const tenantId = getTenantValue(ctx, this.config.tenant);
@@ -1171,7 +1284,7 @@ var RedemptionRepository = class extends Repository {
1171
1284
  pointsReserved: validation.pointsToRedeem,
1172
1285
  pointsConfirmed: 0,
1173
1286
  discountAmount: validation.discountAmount,
1174
- conversionRate: this.config.conversionRate,
1287
+ pointValueMinor: this.config.pointValueMinor,
1175
1288
  status: "reserved",
1176
1289
  ownerType: input.ownerType,
1177
1290
  ownerId: input.ownerId,
@@ -1481,19 +1594,24 @@ var ReferralRepository = class extends Repository {
1481
1594
  }
1482
1595
  /**
1483
1596
  * Atomic compare-and-set status transition.
1484
- * Returns null if the current status doesn't match `expectedStatus`.
1597
+ *
1598
+ * `null` means ONE thing: the guarded update matched no document, i.e.
1599
+ * the status was not `expectedStatus`. `throwOnNotFound: false` already
1600
+ * returns that `null`, so the bare `} catch { return null; }` that used
1601
+ * to sit here could only ever hide REAL errors — most damagingly a
1602
+ * retryable Mongo `WriteConflict`, which callers then re-threw as a
1603
+ * non-retryable `ValidationError` describing a status that had not
1604
+ * actually gone stale.
1605
+ *
1606
+ * Anything that throws in here is a real error and must propagate.
1485
1607
  */
1486
1608
  async atomicStatusTransition(id, expectedStatus, data, ctx) {
1487
- try {
1488
- return await this.update(id, data, {
1489
- lean: true,
1490
- query: { status: expectedStatus },
1491
- throwOnNotFound: false,
1492
- ...ctx
1493
- });
1494
- } catch {
1495
- return null;
1496
- }
1609
+ return this.update(id, data, {
1610
+ lean: true,
1611
+ query: { status: expectedStatus },
1612
+ throwOnNotFound: false,
1613
+ ...ctx
1614
+ });
1497
1615
  }
1498
1616
  async generateCode(memberId, ctx) {
1499
1617
  assertTenantContext(ctx, this.config.tenant);
@@ -1693,7 +1811,7 @@ var ReferralRepository = class extends Repository {
1693
1811
  if (maxDepth > 1 && referrerRewardPoints > 0) {
1694
1812
  const awards = [];
1695
1813
  let currentId = referrer.referredBy;
1696
- const visited = new Set([referrer._id, referee._id]);
1814
+ const visited = /* @__PURE__ */ new Set([referrer._id, referee._id]);
1697
1815
  for (let depth = 2; depth <= maxDepth && currentId; depth++) {
1698
1816
  if (visited.has(currentId)) break;
1699
1817
  visited.add(currentId);
@@ -1828,9 +1946,7 @@ var TierDefinitionRepository = class extends Repository {
1828
1946
  case "downgraded":
1829
1947
  downgraded++;
1830
1948
  break;
1831
- default:
1832
- unchanged++;
1833
- break;
1949
+ default: unchanged++;
1834
1950
  }
1835
1951
  }
1836
1952
  return {
@@ -2094,4 +2210,4 @@ function createRepositories(input) {
2094
2210
  };
2095
2211
  }
2096
2212
  //#endregion
2097
- export { TenantIsolationError as C, ValidationError as E, SelfReferralError as S, UnmanagedSessionError as T, RedemptionExpiredError as _, PointTransactionRepository as a, ReferralNotFoundError as b, flushPendingLoyaltyEvents as c, DuplicateReferralError as d, InsufficientPointsError as f, MemberNotFoundError as g, MemberInactiveError as h, RedemptionRepository as i, EarningRuleRepository as l, MemberAlreadyEnrolledError as m, TierDefinitionRepository as n, MemberRepository as o, LoyaltyCapabilityError as p, ReferralRepository as r, PENDING_EVENTS as s, createRepositories as t, CircularReferralError as u, RedemptionNotFoundError as v, TierNotFoundError as w, RuleNotFoundError as x, ReferralLimitExceededError as y };
2213
+ export { SessionNotInTransactionError as C, ValidationError as D, UnmanagedSessionError as E, SelfReferralError as S, TierNotFoundError as T, RedemptionExpiredError as _, PointTransactionRepository as a, ReferralNotFoundError as b, flushPendingLoyaltyEvents as c, DuplicateReferralError as d, InsufficientPointsError as f, MemberNotFoundError as g, MemberInactiveError as h, RedemptionRepository as i, EarningRuleRepository as l, MemberAlreadyEnrolledError as m, TierDefinitionRepository as n, MemberRepository as o, LoyaltyCapabilityError as p, ReferralRepository as r, PENDING_EVENTS as s, createRepositories as t, CircularReferralError as u, RedemptionNotFoundError as v, TenantIsolationError as w, RuleNotFoundError as x, ReferralLimitExceededError as y };
@@ -1,3 +1,3 @@
1
- import { _ as TierEvaluationBatch, a as EnrollInput, c as UpdateTierInput, d as EarningResult, f as ExpirationResult, g as TierEvaluation, h as RedemptionValidation, i as EarnPointsInput, l as BalanceSummary, m as PaginatedResult, n as CreateEarningRuleInput, o as LoyaltyContext, p as ListQuery, r as CreateTierInput, s as UpdateEarningRuleInput, t as AdjustPointsInput, u as EarningBreakdown } from "../index-YSsi4e_G.mjs";
2
- import { a as PluginType, c as ResolvedTenant, i as ModelName, l as TenantConfig, n as LoyaltyConfig, o as RepositoryPlugins, s as ResolvedConfig, t as IndexDefinition } from "../config-Dtun1duw.mjs";
3
- export { AdjustPointsInput, BalanceSummary, CreateEarningRuleInput, CreateTierInput, EarnPointsInput, EarningBreakdown, EarningResult, EnrollInput, ExpirationResult, IndexDefinition, ListQuery, LoyaltyConfig, LoyaltyContext, ModelName, PaginatedResult, PluginType, RedemptionValidation, RepositoryPlugins, ResolvedConfig, ResolvedTenant, TenantConfig, TierEvaluation, TierEvaluationBatch, UpdateEarningRuleInput, UpdateTierInput };
1
+ import { _ as TierEvaluationBatch, a as EnrollInput, c as UpdateTierInput, d as EarningResult, f as ExpirationResult, g as TierEvaluation, h as RedemptionValidation, i as EarnPointsInput, l as BalanceSummary, m as PaginatedResult, n as CreateEarningRuleInput, o as LoyaltyContext, p as ListQuery, r as CreateTierInput, s as UpdateEarningRuleInput, t as AdjustPointsInput, u as EarningBreakdown } from "../index-Cp_sXNpG.mjs";
2
+ import { a as ModelName, c as ResolvedConfig, i as LoyaltyShape, l as ResolvedTenant, o as PluginType, r as LoyaltyRuntime, s as RepositoryPlugins, t as IndexDefinition, u as TenantConfig } from "../config-CtkGj3xZ.mjs";
3
+ export { AdjustPointsInput, type BalanceSummary, CreateEarningRuleInput, CreateTierInput, EarnPointsInput, type EarningBreakdown, type EarningResult, EnrollInput, type ExpirationResult, type IndexDefinition, type ListQuery, LoyaltyContext, type LoyaltyRuntime, type LoyaltyShape, type ModelName, type PaginatedResult, type PluginType, type RedemptionValidation, type RepositoryPlugins, type ResolvedConfig, type ResolvedTenant, type TenantConfig, type TierEvaluation, type TierEvaluationBatch, UpdateEarningRuleInput, UpdateTierInput };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@classytic/loyalty",
3
- "version": "0.4.0",
3
+ "version": "0.6.0",
4
4
  "description": "Loyalty points, tiers, earning rules, referrals, and redemption engine for MongoDB",
5
5
  "author": "Classytic",
6
6
  "homepage": "https://www.npmjs.com/package/@classytic/loyalty",
@@ -78,24 +78,27 @@
78
78
  "release": "npm run push -- main && npm run release:tag && npm publish"
79
79
  },
80
80
  "peerDependencies": {
81
- "@classytic/mongokit": ">=3.18.0",
82
- "@classytic/primitives": ">=0.9.0",
83
- "@classytic/repo-core": ">=0.7.0",
81
+ "@classytic/mongokit": ">=3.33.0",
82
+ "@classytic/primitives": ">=0.23.0",
83
+ "@classytic/repo-core": ">=0.24.0",
84
+ "@classytic/validation": ">=0.2.0",
84
85
  "mongoose": ">=9.4.1",
85
86
  "zod": ">=4.0.0"
86
87
  },
87
88
  "devDependencies": {
88
89
  "@biomejs/biome": "^2.4.9",
89
90
  "@classytic/dev-tools": "^0.2.0",
90
- "@classytic/mongokit": "^3.18.0",
91
- "@classytic/primitives": "^0.9.0",
92
- "@classytic/repo-core": "^0.7.0",
91
+ "@classytic/mongokit": "^3.33.0",
92
+ "@classytic/primitives": ">=0.23.0",
93
+ "@classytic/repo-core": ">=0.24.0",
94
+ "@classytic/validation": ">=0.4.0",
93
95
  "@types/node": "^25.5.0",
94
96
  "@vitest/coverage-v8": "^3.2.4",
95
97
  "knip": "^6.3.0",
96
98
  "mongodb-memory-server": "^10.2.3",
97
99
  "mongoose": "^9.4.1",
98
- "tsdown": "^0.21.5",
100
+ "publint": "^0.3.23",
101
+ "tsdown": "^0.22.14",
99
102
  "typescript": "^5.7.0",
100
103
  "vitest": "^3.0.0",
101
104
  "zod": "^4.3.6"
@@ -112,5 +115,5 @@
112
115
  "mongodb",
113
116
  "mongoose"
114
117
  ],
115
- "license": "MIT"
118
+ "license": "SEE LICENSE IN LICENSE"
116
119
  }