@classytic/loyalty 0.1.0 → 0.2.1

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 (43) hide show
  1. package/CHANGELOG.md +68 -0
  2. package/dist/config-DoZPGBfW.d.mts +136 -0
  3. package/dist/{constants-C75GvizO.mjs → constants-DkQYkOK1.mjs} +3 -2
  4. package/dist/domain/enums/index.d.mts +1 -1
  5. package/dist/domain/enums/index.mjs +1 -1
  6. package/dist/events/index.d.mts +2 -2
  7. package/dist/events/index.mjs +3 -3
  8. package/dist/events-BtZOdiPw.mjs +311 -0
  9. package/dist/index-BOWqBSWx.d.mts +281 -0
  10. package/dist/index-C4jFHS7m.d.mts +400 -0
  11. package/dist/index-ClPKmtxh.d.mts +361 -0
  12. package/dist/{index-CaH4GhSF.d.mts → index-DMK8a98J.d.mts} +1 -1
  13. package/dist/index.d.mts +28 -12
  14. package/dist/index.mjs +30 -26
  15. package/dist/loyalty-events-Cyt9TwX6.mjs +50 -0
  16. package/dist/models/index.d.mts +1225 -2
  17. package/dist/models/index.mjs +2 -2
  18. package/dist/{models-CrZPmVxt.mjs → models-uhh9c-7k.mjs} +104 -41
  19. package/dist/notifications/index.d.mts +1 -1
  20. package/dist/repositories/index.d.mts +2 -2
  21. package/dist/repositories/index.mjs +1 -1
  22. package/dist/repositories-DBSxe9st.mjs +1919 -0
  23. package/dist/types/index.d.mts +2 -2
  24. package/package.json +22 -17
  25. package/dist/config-aNigy4ec.d.mts +0 -90
  26. package/dist/domain/index.d.mts +0 -90
  27. package/dist/domain/index.mjs +0 -4
  28. package/dist/domain-errors-D7q-2LYD.mjs +0 -120
  29. package/dist/event-emitter.port-CLYPoMni.d.mts +0 -8
  30. package/dist/event-types-xQBtYBbX.mjs +0 -24
  31. package/dist/events-CprEWlN7.mjs +0 -25
  32. package/dist/index-B6ZstxSP.d.mts +0 -2357
  33. package/dist/index-BcoGeDco.d.mts +0 -127
  34. package/dist/index-DqzTjeEB.d.mts +0 -148
  35. package/dist/index-O7TL4R2P.d.mts +0 -86
  36. package/dist/index-_pg4J11t.d.mts +0 -191
  37. package/dist/repositories-qMX7Xh_y.mjs +0 -440
  38. package/dist/services/index.d.mts +0 -2
  39. package/dist/services/index.mjs +0 -2
  40. package/dist/services-CoTqC4E6.mjs +0 -1098
  41. package/dist/tier-definition-Djf72UPa.d.mts +0 -120
  42. package/dist/tier-definition.port-BzrHYBRo.d.mts +0 -143
  43. /package/dist/{notification.port-BzUsH0It.d.mts → notification.port-CJ9nXj0k.d.mts} +0 -0
@@ -0,0 +1,361 @@
1
+ import { i as ReferralStatus, r as RedemptionStatus } from "./index-DMK8a98J.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-BOWqBSWx.mjs";
3
+ import { t as NotificationAdapter } from "./notification.port-CJ9nXj0k.mjs";
4
+ import { c as ResolvedTenant, o as RepositoryPlugins, s as ResolvedConfig } from "./config-DoZPGBfW.mjs";
5
+ import { LoyaltyModels } from "./models/index.mjs";
6
+ import { EventTransport } from "@classytic/primitives/events";
7
+ import { ClientSession, Model } from "mongoose";
8
+ import { PluginType, Repository } from "@classytic/mongokit";
9
+ import { OutboxStore } from "@classytic/primitives/outbox";
10
+
11
+ //#region src/domain/ports/unit-of-work.port.d.ts
12
+ interface UnitOfWork {
13
+ withTransaction<T>(callback: (session: ClientSession) => Promise<T>): Promise<T>;
14
+ }
15
+ //#endregion
16
+ //#region src/repositories/member.repository.d.ts
17
+ declare class MemberRepository extends Repository<Member> {
18
+ events: EventTransport;
19
+ outbox: OutboxStore | undefined;
20
+ notifications: NotificationAdapter | null;
21
+ config: ResolvedConfig;
22
+ /** Session-bound outbox save + transport publish (PACKAGE_RULES §5.5 + P8). */
23
+ private _dispatch;
24
+ constructor(model: Model<Member>, plugins?: PluginType[]);
25
+ /**
26
+ * Auto-inject the tenant id from the repo options into the doc so writes
27
+ * persist with the correct `organizationId` even when the host has opted
28
+ * OUT of the auto-wired `multiTenantPlugin` (e.g. arc hosts that scope at
29
+ * the framework layer). Matches `@classytic/order` / `@classytic/promo`.
30
+ * No-op when the payload already carries the field.
31
+ */
32
+ create(data: Parameters<Repository<Member>['create']>[0], options?: Parameters<Repository<Member>['create']>[1]): Promise<Member>;
33
+ createMany(docs: Parameters<Repository<Member>['createMany']>[0], options?: Parameters<Repository<Member>['createMany']>[1]): Promise<Member[]>;
34
+ /**
35
+ * Atomic balance increment -- uses $inc to prevent TOCTOU races.
36
+ * Returns the updated member document.
37
+ */
38
+ updateBalance(id: string, balanceUpdate: Record<string, number>, ctx?: Record<string, unknown>): Promise<Member>;
39
+ /** Fetches a member by ID (tenant-scoped via plugin) or throws MemberNotFoundError. */
40
+ requireMember(memberId: string, ctx?: Record<string, unknown>): Promise<Member>;
41
+ /** Fetches an active member (tenant-scoped via plugin) or throws MemberNotFoundError / MemberInactiveError. */
42
+ requireActiveMember(memberId: string, ctx?: Record<string, unknown>): Promise<Member>;
43
+ enroll(input: {
44
+ externalId: string;
45
+ externalType: string;
46
+ programId?: string;
47
+ tier?: string;
48
+ cardId?: string;
49
+ metadata?: Record<string, unknown>;
50
+ }, ctx: LoyaltyContext): Promise<Member>;
51
+ deactivate(memberId: string, ctx: LoyaltyContext): Promise<Member>;
52
+ reactivate(memberId: string, ctx: LoyaltyContext): Promise<Member>;
53
+ suspend(memberId: string, reason: string, ctx: LoyaltyContext): Promise<Member>;
54
+ }
55
+ //#endregion
56
+ //#region src/repositories/point-transaction.repository.d.ts
57
+ declare class PointTransactionRepository extends Repository<PointTransaction> {
58
+ memberRepo: MemberRepository;
59
+ events: EventTransport;
60
+ outbox: OutboxStore | undefined;
61
+ notifications: NotificationAdapter | null;
62
+ unitOfWork: UnitOfWork;
63
+ config: ResolvedConfig;
64
+ constructor(model: Model<PointTransaction>, plugins?: PluginType[]);
65
+ /** Session-bound outbox save + transport publish (PACKAGE_RULES §5.5 + P8). */
66
+ private _dispatch;
67
+ /**
68
+ * Auto-inject tenant id from options into doc payload. Matches
69
+ * `@classytic/order` / `@classytic/promo` — see MemberRepository.
70
+ */
71
+ create(data: Parameters<Repository<PointTransaction>['create']>[0], options?: Parameters<Repository<PointTransaction>['create']>[1]): Promise<PointTransaction>;
72
+ createMany(docs: Parameters<Repository<PointTransaction>['createMany']>[0], options?: Parameters<Repository<PointTransaction>['createMany']>[1]): Promise<PointTransaction[]>;
73
+ earnPoints(input: {
74
+ memberId: string;
75
+ points: number;
76
+ description: string;
77
+ referenceType?: string;
78
+ referenceId?: string;
79
+ earningRuleId?: string;
80
+ expiresAt?: Date;
81
+ idempotencyKey?: string;
82
+ metadata?: Record<string, unknown>;
83
+ }, ctx: LoyaltyContext): Promise<PointTransaction>;
84
+ /**
85
+ * Fans out `earnPoints` across many members for bulk-issuance campaigns
86
+ * (welcome bonuses, holiday boosts, win-back). Per-member failures do
87
+ * NOT abort the batch — they surface in `errors[]` with the memberId +
88
+ * message, matching `@classytic/commission`'s `ingestSplit` shape.
89
+ *
90
+ * Idempotency: each row derives its key as `${source.eventId}:${memberId}`
91
+ * so retrying the whole batch collapses onto the same transactions.
92
+ *
93
+ * Events: emits exactly ONE `CAMPAIGN_AWARDED` with aggregate counts —
94
+ * NOT one-per-member, to avoid an event storm for large batches. The
95
+ * underlying `POINTS_EARNED` events still fire (one per successful row)
96
+ * for ledger consumers that need per-member granularity.
97
+ */
98
+ awardBatch(members: Array<{
99
+ memberId: string;
100
+ points: number;
101
+ }>, source: {
102
+ eventId: string;
103
+ campaignId: string;
104
+ description: string;
105
+ referenceType?: string;
106
+ referenceId?: string;
107
+ expiresAt?: Date;
108
+ metadata?: Record<string, unknown>;
109
+ }, ctx: LoyaltyContext): Promise<{
110
+ awarded: number;
111
+ skipped: number;
112
+ errors: Array<{
113
+ memberId: string;
114
+ error: string;
115
+ }>;
116
+ totalPoints: number;
117
+ }>;
118
+ adjustPoints(input: {
119
+ memberId: string;
120
+ points: number;
121
+ description: string;
122
+ reason: string;
123
+ referenceType?: string;
124
+ referenceId?: string;
125
+ idempotencyKey?: string;
126
+ metadata?: Record<string, unknown>;
127
+ }, ctx: LoyaltyContext): Promise<PointTransaction>;
128
+ processExpirations(ctx: LoyaltyContext): Promise<ExpirationResult>;
129
+ }
130
+ //#endregion
131
+ //#region src/repositories/earning-rule.repository.d.ts
132
+ declare class EarningRuleRepository extends Repository<EarningRule> {
133
+ memberRepo: MemberRepository;
134
+ txRepo: PointTransactionRepository;
135
+ config: ResolvedConfig;
136
+ constructor(model: Model<EarningRule>, plugins?: PluginType[]);
137
+ /** Auto-inject tenant id from options into doc payload. See MemberRepository. */
138
+ create(data: Parameters<Repository<EarningRule>['create']>[0], options?: Parameters<Repository<EarningRule>['create']>[1]): Promise<EarningRule>;
139
+ createMany(docs: Parameters<Repository<EarningRule>['createMany']>[0], options?: Parameters<Repository<EarningRule>['createMany']>[1]): Promise<EarningRule[]>;
140
+ evaluateOrder(input: {
141
+ memberId: string;
142
+ orderId: string;
143
+ orderTotal: number;
144
+ items?: Array<{
145
+ categoryId?: string;
146
+ amount: number;
147
+ quantity: number;
148
+ }>;
149
+ }, ctx: LoyaltyContext): Promise<EarningResult>;
150
+ evaluateAction(input: {
151
+ memberId: string;
152
+ action: string;
153
+ referenceId?: string;
154
+ }, ctx: LoyaltyContext): Promise<EarningResult>;
155
+ private matchesConditions;
156
+ private calculateOrderPoints;
157
+ private calculateCategoryPoints;
158
+ private applyRounding;
159
+ }
160
+ //#endregion
161
+ //#region src/domain/entities/redemption.d.ts
162
+ interface Redemption {
163
+ _id: string;
164
+ memberId: string;
165
+ programId: string;
166
+ pointsReserved: number;
167
+ pointsConfirmed: number;
168
+ discountAmount: number;
169
+ conversionRate: number;
170
+ status: RedemptionStatus;
171
+ ownerType: string;
172
+ ownerId: string;
173
+ expiresAt?: Date;
174
+ confirmedAt?: Date;
175
+ releasedAt?: Date;
176
+ reversedAt?: Date;
177
+ reversalReason?: string;
178
+ /** Point transaction id for the compensating credit created by `reverseConfirmed`. */
179
+ reversalTransactionId?: string;
180
+ /** Point transaction id for the `redeem` debit created at `confirm()` time. */
181
+ transactionId?: string;
182
+ /**
183
+ * Optional voucher-like partial-use tracking populated by hosts when
184
+ * the redemption is backed by a gift voucher / reward card. When
185
+ * present, `reverseConfirmed({ proportional: true })` credits back
186
+ * `(remainingBalance / initialBalance) × pointsConfirmed`.
187
+ */
188
+ initialBalance?: number;
189
+ remainingBalance?: number;
190
+ createdAt: Date;
191
+ updatedAt: Date;
192
+ }
193
+ //#endregion
194
+ //#region src/repositories/redemption.repository.d.ts
195
+ declare class RedemptionRepository extends Repository<Redemption> {
196
+ memberRepo: MemberRepository;
197
+ txRepo: PointTransactionRepository;
198
+ events: EventTransport;
199
+ outbox: OutboxStore | undefined;
200
+ notifications: NotificationAdapter | null;
201
+ unitOfWork: UnitOfWork;
202
+ config: ResolvedConfig;
203
+ constructor(model: Model<Redemption>, plugins?: PluginType[]);
204
+ /** Auto-inject tenant id from options into doc payload. See MemberRepository. */
205
+ create(data: Parameters<Repository<Redemption>['create']>[0], options?: Parameters<Repository<Redemption>['create']>[1]): Promise<Redemption>;
206
+ createMany(docs: Parameters<Repository<Redemption>['createMany']>[0], options?: Parameters<Repository<Redemption>['createMany']>[1]): Promise<Redemption[]>;
207
+ /** Session-bound outbox save + transport publish (PACKAGE_RULES §5.5 + P8). */
208
+ private _dispatch;
209
+ /**
210
+ * Atomic compare-and-set status transition.
211
+ * Returns null if the current status doesn't match `expectedStatus` (CAS mismatch).
212
+ */
213
+ atomicStatusTransition(id: string, expectedStatus: string, data: Record<string, unknown>, ctx?: Record<string, unknown>): Promise<Redemption | null>;
214
+ validate(input: {
215
+ memberId: string;
216
+ pointsToRedeem: number;
217
+ orderTotal: number;
218
+ }, ctx: LoyaltyContext): Promise<RedemptionValidation>;
219
+ reserve(input: {
220
+ memberId: string;
221
+ pointsToRedeem: number;
222
+ orderTotal: number;
223
+ ownerType: string;
224
+ ownerId: string;
225
+ expiresAt?: Date;
226
+ }, ctx: LoyaltyContext): Promise<Redemption>;
227
+ confirm(redemptionId: string, ctx: LoyaltyContext): Promise<Redemption>;
228
+ release(redemptionId: string, ctx: LoyaltyContext): Promise<Redemption>;
229
+ /**
230
+ * Reverses a confirmed redemption. Credits points back to the member as
231
+ * a compensating `adjust` transaction, marks the redemption `reversed`,
232
+ * and emits `REDEMPTION_REVERSED`. Guarded by an atomic CAS on
233
+ * `status === 'confirmed'` so idempotent callers never double-credit.
234
+ *
235
+ * @param options.proportional - when true + the redemption has a
236
+ * `remainingBalance` + `initialBalance` (voucher-backed), credits
237
+ * back `(remainingBalance / initialBalance) × pointsConfirmed`.
238
+ * When false (default), credits back the full `pointsConfirmed`.
239
+ * When true but the redemption has no voucher metadata, falls back
240
+ * to a full reversal and stamps `metadata.proportional_fallback`.
241
+ * @param options.reason - free-text audit note; persisted on the
242
+ * redemption + the compensating point_transaction metadata.
243
+ */
244
+ reverseConfirmed(redemptionId: string, ctx: LoyaltyContext, options?: {
245
+ proportional?: boolean;
246
+ reason?: string;
247
+ }): Promise<Redemption>;
248
+ cleanupExpired(ctx: LoyaltyContext): Promise<number>;
249
+ }
250
+ //#endregion
251
+ //#region src/domain/entities/referral.d.ts
252
+ interface Referral {
253
+ _id: string;
254
+ programId: string;
255
+ referrerId: string;
256
+ refereeId: string;
257
+ referralCode: string;
258
+ status: ReferralStatus;
259
+ referrerRewardPoints?: number;
260
+ refereeRewardPoints?: number;
261
+ referrerTransactionId?: string;
262
+ refereeTransactionId?: string;
263
+ approvedAt?: Date;
264
+ rejectedAt?: Date;
265
+ rejectionReason?: string;
266
+ expiresAt?: Date;
267
+ metadata?: Record<string, unknown>;
268
+ createdAt: Date;
269
+ updatedAt: Date;
270
+ }
271
+ //#endregion
272
+ //#region src/repositories/referral.repository.d.ts
273
+ declare class ReferralRepository extends Repository<Referral> {
274
+ memberRepo: MemberRepository;
275
+ txRepo: PointTransactionRepository;
276
+ events: EventTransport;
277
+ outbox: OutboxStore | undefined;
278
+ notifications: NotificationAdapter | null;
279
+ config: ResolvedConfig;
280
+ constructor(model: Model<Referral>, plugins?: PluginType[]);
281
+ /** Session-bound outbox save + transport publish (PACKAGE_RULES §5.5 + P8). */
282
+ private _dispatch;
283
+ /** Auto-inject tenant id from options into doc payload. See MemberRepository. */
284
+ create(data: Parameters<Repository<Referral>['create']>[0], options?: Parameters<Repository<Referral>['create']>[1]): Promise<Referral>;
285
+ createMany(docs: Parameters<Repository<Referral>['createMany']>[0], options?: Parameters<Repository<Referral>['createMany']>[1]): Promise<Referral[]>;
286
+ /**
287
+ * Atomic compare-and-set status transition.
288
+ * Returns null if the current status doesn't match `expectedStatus`.
289
+ */
290
+ atomicStatusTransition(id: string, expectedStatus: string, data: Record<string, unknown>, ctx?: Record<string, unknown>): Promise<Referral | null>;
291
+ generateCode(memberId: string, ctx: LoyaltyContext): Promise<string>;
292
+ lookupByCode(code: string, ctx: LoyaltyContext): Promise<Member | null>;
293
+ recordReferral(input: {
294
+ referralCode: string;
295
+ refereeExternalId: string;
296
+ refereeExternalType: string;
297
+ }, ctx: LoyaltyContext): Promise<Referral>;
298
+ approve(referralId: string, ctx: LoyaltyContext): Promise<Referral>;
299
+ reject(referralId: string, reason: string, ctx: LoyaltyContext): Promise<Referral>;
300
+ cleanupExpired(ctx: LoyaltyContext): Promise<number>;
301
+ private rewardBothSides;
302
+ private requireReferral;
303
+ }
304
+ //#endregion
305
+ //#region src/repositories/tier-definition.repository.d.ts
306
+ declare class TierDefinitionRepository extends Repository<TierDefinition> {
307
+ memberRepo: MemberRepository;
308
+ events: EventTransport;
309
+ outbox: OutboxStore | undefined;
310
+ notifications: NotificationAdapter | null;
311
+ config: ResolvedConfig;
312
+ constructor(model: Model<TierDefinition>, plugins?: PluginType[]);
313
+ /** Auto-inject tenant id from options into doc payload. See MemberRepository. */
314
+ create(data: Parameters<Repository<TierDefinition>['create']>[0], options?: Parameters<Repository<TierDefinition>['create']>[1]): Promise<TierDefinition>;
315
+ createMany(docs: Parameters<Repository<TierDefinition>['createMany']>[0], options?: Parameters<Repository<TierDefinition>['createMany']>[1]): Promise<TierDefinition[]>;
316
+ /** Session-bound outbox save + transport publish (PACKAGE_RULES §5.5 + P8). */
317
+ private _dispatch;
318
+ evaluateMember(memberId: string, ctx: LoyaltyContext): Promise<TierEvaluation>;
319
+ evaluateAll(ctx: LoyaltyContext): Promise<TierEvaluationBatch>;
320
+ setOverride(memberId: string, tierName: string, reason: string, ctx: LoyaltyContext, options?: {
321
+ expiresAt?: Date;
322
+ }): Promise<Member>;
323
+ clearOverride(memberId: string, ctx: LoyaltyContext): Promise<TierEvaluation>;
324
+ /**
325
+ * Sweeps members whose `tierOverrideExpiresAt` is past `now` and clears
326
+ * the override. Emits `TIER_OVERRIDE_EXPIRED` per member — distinct from
327
+ * the explicit `TIER_OVERRIDE_CLEARED` event — so audit logs can tell
328
+ * admin-initiated clears from TTL sweeps.
329
+ *
330
+ * Hosts call this on a cron schedule (e.g. hourly). Returns the number
331
+ * of overrides cleared. No-op when nothing matches.
332
+ */
333
+ sweepExpiredOverrides(ctx: LoyaltyContext, now?: Date): Promise<{
334
+ cleared: number;
335
+ }>;
336
+ private applyUpgrade;
337
+ private applyDowngrade;
338
+ }
339
+ //#endregion
340
+ //#region src/repositories/index.d.ts
341
+ interface LoyaltyRepositories {
342
+ member: MemberRepository;
343
+ pointTransaction: PointTransactionRepository;
344
+ earningRule: EarningRuleRepository;
345
+ tierDefinition: TierDefinitionRepository;
346
+ redemption: RedemptionRepository;
347
+ referral: ReferralRepository;
348
+ }
349
+ interface CreateRepositoriesInput {
350
+ models: LoyaltyModels;
351
+ plugins?: RepositoryPlugins;
352
+ tenant?: ResolvedTenant;
353
+ config: ResolvedConfig;
354
+ events: EventTransport;
355
+ outbox?: OutboxStore | undefined;
356
+ notifications: NotificationAdapter | null;
357
+ unitOfWork: UnitOfWork;
358
+ }
359
+ declare function createRepositories(input: CreateRepositoriesInput): LoyaltyRepositories;
360
+ //#endregion
361
+ 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 };
@@ -4,7 +4,7 @@ declare const MEMBER_STATUSES: readonly ["active", "inactive", "suspended"];
4
4
  declare const RULE_TYPES: readonly ["order", "action", "category", "tier_bonus"];
5
5
  declare const RULE_STATUSES: readonly ["active", "paused", "expired"];
6
6
  declare const REFERRAL_STATUSES: readonly ["pending", "approved", "rejected", "expired"];
7
- declare const REDEMPTION_STATUSES: readonly ["reserved", "confirmed", "released", "expired"];
7
+ declare const REDEMPTION_STATUSES: readonly ["reserved", "confirmed", "released", "expired", "reversed"];
8
8
  declare const EARNING_ACTIONS: readonly ["signup", "referral", "birthday", "review", "profile_complete"];
9
9
  //#endregion
10
10
  //#region src/domain/enums/index.d.ts
package/dist/index.d.mts CHANGED
@@ -1,21 +1,37 @@
1
- import { a as PointTransaction, n as TierDefinition, o as Member, s as MemberBalance, u as EarningRule } from "./tier-definition-Djf72UPa.mjs";
2
- import { l as Referral, u as Redemption } from "./tier-definition.port-BzrHYBRo.mjs";
3
- import { t as EventEmitterPort } from "./event-emitter.port-CLYPoMni.mjs";
4
- import { n as LoyaltyEvents } from "./index-O7TL4R2P.mjs";
5
- import { n as LoyaltyConfig, o as ResolvedConfig, s as ResolvedTenant } from "./config-aNigy4ec.mjs";
6
- import { t as LoyaltyModels } from "./index-B6ZstxSP.mjs";
7
- import { t as LoyaltyRepositories } from "./index-BcoGeDco.mjs";
8
- import { a as EnrollInput, c as UpdateTierInput, d as EarningResult, g as TierEvaluation, h as RedemptionValidation, i as EarnPointsInput, l as BalanceSummary, n as CreateEarningRuleInput, o as LoyaltyContext, r as CreateTierInput, s as UpdateEarningRuleInput, t as AdjustPointsInput } from "./index-DqzTjeEB.mjs";
9
- import { n as LoyaltyServices } from "./index-_pg4J11t.mjs";
1
+ import { S as EarningRule, a as EnrollInput, b as MemberBalance, c as UpdateTierInput, d as EarningResult, g as TierEvaluation, h as RedemptionValidation, i as EarnPointsInput, l as BalanceSummary, n as CreateEarningRuleInput, o as LoyaltyContext, r as CreateTierInput, s as UpdateEarningRuleInput, t as AdjustPointsInput, v as PointTransaction, x as TierDefinition, y as Member } from "./index-BOWqBSWx.mjs";
2
+ import { t as NotificationAdapter } from "./notification.port-CJ9nXj0k.mjs";
3
+ import { c as ResolvedTenant, n as LoyaltyConfig, r as LoyaltyModelName, s as ResolvedConfig } from "./config-DoZPGBfW.mjs";
4
+ import { A as PointsExpired, B as ReferralRewarded, C as MemberDeactivated, E as MemberSuspended, F as RedemptionReserved, G as TierUpgraded, H as TierGraceStarted, J as InProcessLoyaltyBusOptions, K as loyaltyEventDefinitions, L as ReferralApproved, M as PointsRedeemed, N as RedemptionConfirmed, O as PointsAdjusted, P as RedemptionReleased, R as ReferralRecorded, S as LoyaltyEventSchema, T as MemberReactivated, V as TierDowngraded, Y as createEvent, _ as TierChangedPayload, a as LoyaltyEventMap, b as LoyaltyEventDefinition, c as LoyaltyEvents, d as PointsAdjustedPayload, f as PointsEarnedPayload, g as ReferralPayload, j as PointsExpiringSoon, k as PointsEarned, l as MemberLifecyclePayload, m as RedemptionPayload, o as LoyaltyEventName, p as PointsExpiredPayload, q as InProcessLoyaltyBus, s as LoyaltyEventPayload, w as MemberEnrolled, x as LoyaltyEventPayloadOf, z as ReferralRejected } from "./index-C4jFHS7m.mjs";
5
+ import { LoyaltyModels } from "./models/index.mjs";
6
+ import { a as ReferralRepository, c as Redemption, d as MemberRepository, f as UnitOfWork, i as TierDefinitionRepository, l as EarningRuleRepository, n as LoyaltyRepositories, o as Referral, s as RedemptionRepository, u as PointTransactionRepository } from "./index-ClPKmtxh.mjs";
7
+ import { DomainEvent, EventHandler, EventTransport, EventTransport as EventTransport$1 } from "@classytic/primitives/events";
10
8
 
11
9
  //#region src/index.d.ts
12
10
  declare function resolveConfig(config: LoyaltyConfig): ResolvedConfig;
11
+ /**
12
+ * LoyaltyEngine -- repositories + arc-compatible event transport.
13
+ *
14
+ * Domain verbs live on the repositories themselves (`engine.repositories.member.enroll(...)`,
15
+ * `engine.repositories.pointTransaction.earnPoints(...)`, etc.). There is no separate
16
+ * service layer. See PACKAGE_RULES S1-S3.
17
+ *
18
+ * `events` is structurally identical to `@classytic/arc`'s `EventTransport`.
19
+ * Hosts can pass any arc transport (Memory/Redis/Kafka) via `eventTransport`
20
+ * and subscribe glob-style:
21
+ *
22
+ * const loyalty = createLoyaltyEngine({
23
+ * mongoose,
24
+ * eventTransport: new RedisEventTransport({ url }),
25
+ * });
26
+ *
27
+ * await loyalty.events.subscribe('loyalty.points.*', handler);
28
+ */
13
29
  interface LoyaltyEngine {
14
30
  models: LoyaltyModels;
15
31
  repositories: LoyaltyRepositories;
16
- services: LoyaltyServices;
17
- events: EventEmitterPort;
32
+ events: EventTransport$1;
33
+ syncIndexes(): Promise<void>;
18
34
  }
19
35
  declare function createLoyaltyEngine(config: LoyaltyConfig): LoyaltyEngine;
20
36
  //#endregion
21
- export { type AdjustPointsInput, type BalanceSummary, type CreateEarningRuleInput, type CreateTierInput, type EarnPointsInput, type EarningResult, type EarningRule, type EnrollInput, type EventEmitterPort, type LoyaltyConfig, type LoyaltyContext, LoyaltyEngine, LoyaltyEvents, type LoyaltyModels, type LoyaltyRepositories, type LoyaltyServices, type Member, type MemberBalance, type PointTransaction, type Redemption, type RedemptionValidation, type Referral, type ResolvedConfig, type ResolvedTenant, type TierDefinition, type TierEvaluation, type UpdateEarningRuleInput, type UpdateTierInput, createLoyaltyEngine, resolveConfig };
37
+ export { type AdjustPointsInput, type BalanceSummary, type CreateEarningRuleInput, type CreateTierInput, type DomainEvent, type EarnPointsInput, type EarningResult, type EarningRule, EarningRuleRepository, type EnrollInput, type EventHandler, type EventTransport, InProcessLoyaltyBus, type InProcessLoyaltyBusOptions, type LoyaltyConfig, type LoyaltyContext, LoyaltyEngine, type LoyaltyEventDefinition, type LoyaltyEventMap, type LoyaltyEventName, type LoyaltyEventPayload, type LoyaltyEventPayloadOf, type LoyaltyEventSchema, LoyaltyEvents, type LoyaltyModelName, type LoyaltyModels, type LoyaltyRepositories, type Member, type MemberBalance, MemberDeactivated, MemberEnrolled, type MemberLifecyclePayload, MemberReactivated, MemberRepository, MemberSuspended, type NotificationAdapter, type PointTransaction, PointTransactionRepository, PointsAdjusted, type PointsAdjustedPayload, PointsEarned, type PointsEarnedPayload, PointsExpired, type PointsExpiredPayload, PointsExpiringSoon, PointsRedeemed, type Redemption, RedemptionConfirmed, type RedemptionPayload, RedemptionReleased, RedemptionRepository, RedemptionReserved, type RedemptionValidation, type Referral, ReferralApproved, type ReferralPayload, ReferralRecorded, ReferralRejected, ReferralRepository, ReferralRewarded, type ResolvedConfig, type ResolvedTenant, type TierChangedPayload, type TierDefinition, TierDefinitionRepository, TierDowngraded, type TierEvaluation, TierGraceStarted, TierUpgraded, type UnitOfWork, type UpdateEarningRuleInput, type UpdateTierInput, createEvent, createLoyaltyEngine, loyaltyEventDefinitions, resolveConfig };
package/dist/index.mjs CHANGED
@@ -1,30 +1,28 @@
1
- import { n as DEFAULT_TENANT_FIELD, r as DEFAULT_TENANT_TYPE } from "./constants-C75GvizO.mjs";
2
- import { h as ValidationError } from "./domain-errors-D7q-2LYD.mjs";
3
- import { t as InProcessEventBus } from "./events-CprEWlN7.mjs";
4
- import { t as createModels } from "./models-CrZPmVxt.mjs";
5
- import { t as createRepositories } from "./repositories-qMX7Xh_y.mjs";
6
- import { t as createServices } from "./services-CoTqC4E6.mjs";
7
- import { t as LoyaltyEvents } from "./event-types-xQBtYBbX.mjs";
1
+ import { n as DEFAULT_TENANT_FIELD, r as DEFAULT_TENANT_TYPE } from "./constants-DkQYkOK1.mjs";
2
+ import { a as PointTransactionRepository, c as ValidationError, i as RedemptionRepository, n as TierDefinitionRepository, o as MemberRepository, r as ReferralRepository, s as EarningRuleRepository, t as createRepositories } from "./repositories-DBSxe9st.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-BtZOdiPw.mjs";
4
+ import { n as createModels } from "./models-uhh9c-7k.mjs";
5
+ import { n as createEvent, t as LoyaltyEvents } from "./loyalty-events-Cyt9TwX6.mjs";
8
6
  //#region src/index.ts
9
7
  function resolveConfig(config) {
10
8
  let tenant;
11
9
  if (config.tenant === false) tenant = {
12
10
  enabled: false,
13
- field: "",
14
- type: "ObjectId",
15
- contextKey: ""
11
+ tenantField: DEFAULT_TENANT_FIELD,
12
+ fieldType: DEFAULT_TENANT_TYPE,
13
+ contextKey: DEFAULT_TENANT_FIELD
16
14
  };
17
15
  else if (config.tenant === void 0) tenant = {
18
16
  enabled: true,
19
- field: DEFAULT_TENANT_FIELD,
20
- type: DEFAULT_TENANT_TYPE,
17
+ tenantField: DEFAULT_TENANT_FIELD,
18
+ fieldType: DEFAULT_TENANT_TYPE,
21
19
  contextKey: DEFAULT_TENANT_FIELD
22
20
  };
23
21
  else tenant = {
24
22
  enabled: true,
25
- field: config.tenant.field ?? "organizationId",
26
- type: config.tenant.type ?? "ObjectId",
27
- contextKey: config.tenant.contextKey ?? config.tenant.field ?? "organizationId",
23
+ tenantField: config.tenant.tenantField ?? "organizationId",
24
+ fieldType: config.tenant.fieldType ?? "objectId",
25
+ contextKey: config.tenant.contextKey ?? config.tenant.tenantField ?? "organizationId",
28
26
  ...config.tenant.ref ? { ref: config.tenant.ref } : {}
29
27
  };
30
28
  const resolved = {
@@ -42,7 +40,9 @@ function resolveConfig(config) {
42
40
  maxPerPeriod: config.referral?.maxPerPeriod ?? 10,
43
41
  periodDays: config.referral?.periodDays ?? 30,
44
42
  requireApproval: config.referral?.requireApproval ?? false,
45
- codeLength: config.referral?.codeLength ?? 8
43
+ codeLength: config.referral?.codeLength ?? 8,
44
+ maxDepth: Math.max(1, config.referral?.maxDepth ?? 1),
45
+ tierRates: config.referral?.tierRates ?? [100]
46
46
  },
47
47
  expiry: {
48
48
  enabled: config.expiry?.enabled ?? false,
@@ -74,22 +74,26 @@ var MongoUnitOfWork = class {
74
74
  };
75
75
  function createLoyaltyEngine(config) {
76
76
  const resolvedConfig = resolveConfig(config);
77
- const models = createModels(config.mongoose, resolvedConfig.tenant, config.indexes);
78
- const repositories = createRepositories(models, config.plugins, resolvedConfig.tenant.field);
77
+ const models = createModels(config.mongoose, resolvedConfig.tenant, config.indexes, config.autoIndex, config.collectionPrefix, config.forceRecreate);
79
78
  const unitOfWork = new MongoUnitOfWork(config.mongoose);
80
- const events = config.events?.adapter ?? new InProcessEventBus();
79
+ const events = config.eventTransport ?? new InProcessLoyaltyBus();
81
80
  return {
82
81
  models,
83
- repositories,
84
- services: createServices({
85
- repositories,
86
- unitOfWork,
82
+ repositories: createRepositories({
83
+ models,
84
+ plugins: config.plugins,
85
+ tenant: resolvedConfig.tenant,
86
+ config: resolvedConfig,
87
87
  events,
88
+ outbox: config.outbox,
88
89
  notifications: config.notifications ?? null,
89
- config: resolvedConfig
90
+ unitOfWork
90
91
  }),
91
- events
92
+ events,
93
+ async syncIndexes() {
94
+ await Promise.all(Object.values(models).map((m) => m.createIndexes()));
95
+ }
92
96
  };
93
97
  }
94
98
  //#endregion
95
- export { LoyaltyEvents, createLoyaltyEngine, resolveConfig };
99
+ export { EarningRuleRepository, InProcessLoyaltyBus, LoyaltyEvents, MemberDeactivated, MemberEnrolled, MemberReactivated, MemberRepository, MemberSuspended, PointTransactionRepository, PointsAdjusted, PointsEarned, PointsExpired, PointsExpiringSoon, PointsRedeemed, RedemptionConfirmed, RedemptionReleased, RedemptionRepository, RedemptionReserved, ReferralApproved, ReferralRecorded, ReferralRejected, ReferralRepository, ReferralRewarded, TierDefinitionRepository, TierDowngraded, TierGraceStarted, TierUpgraded, createEvent, createLoyaltyEngine, loyaltyEventDefinitions, resolveConfig };
@@ -0,0 +1,50 @@
1
+ import { createEvent } from "@classytic/primitives/events";
2
+ //#region src/events/helpers.ts
3
+ function createEvent$1(type, payload, ctx, meta) {
4
+ return createEvent(type, payload, {
5
+ userId: ctx?.actorId,
6
+ organizationId: ctx?.organizationId,
7
+ ...meta
8
+ });
9
+ }
10
+ //#endregion
11
+ //#region src/events/loyalty-events.ts
12
+ /**
13
+ * Loyalty event name + payload map.
14
+ *
15
+ * Minimal surface. Loyalty emits events for its own lifecycle transitions
16
+ * only — member, points ledger, tier, redemption, and referral.
17
+ *
18
+ * For registration with an arc-compatible event registry, see
19
+ * `loyalty-event-catalog.ts` in this directory — it exposes each event as
20
+ * a `LoyaltyEventDefinition` with a Zod payload schema + JSON Schema
21
+ * derived via `z.toJSONSchema()` (PACKAGE_RULES §18.5).
22
+ */
23
+ const LoyaltyEvents = {
24
+ MEMBER_ENROLLED: "loyalty.member.enrolled",
25
+ MEMBER_DEACTIVATED: "loyalty.member.deactivated",
26
+ MEMBER_REACTIVATED: "loyalty.member.reactivated",
27
+ MEMBER_SUSPENDED: "loyalty.member.suspended",
28
+ POINTS_EARNED: "loyalty.points.earned",
29
+ POINTS_REDEEMED: "loyalty.points.redeemed",
30
+ POINTS_ADJUSTED: "loyalty.points.adjusted",
31
+ POINTS_EXPIRED: "loyalty.points.expired",
32
+ POINTS_EXPIRING_SOON: "loyalty.points.expiring_soon",
33
+ TIER_UPGRADED: "loyalty.tier.upgraded",
34
+ TIER_DOWNGRADED: "loyalty.tier.downgraded",
35
+ TIER_GRACE_STARTED: "loyalty.tier.grace_started",
36
+ TIER_OVERRIDE_CLEARED: "loyalty.tier.override_cleared",
37
+ TIER_OVERRIDE_EXPIRED: "loyalty.tier.override_expired",
38
+ REDEMPTION_RESERVED: "loyalty.redemption.reserved",
39
+ REDEMPTION_CONFIRMED: "loyalty.redemption.confirmed",
40
+ REDEMPTION_RELEASED: "loyalty.redemption.released",
41
+ REDEMPTION_REVERSED: "loyalty.redemption.reversed",
42
+ CAMPAIGN_AWARDED: "loyalty.campaign.awarded",
43
+ REFERRAL_RECORDED: "loyalty.referral.recorded",
44
+ REFERRAL_APPROVED: "loyalty.referral.approved",
45
+ REFERRAL_REJECTED: "loyalty.referral.rejected",
46
+ REFERRAL_REWARDED: "loyalty.referral.rewarded",
47
+ MULTI_TIER_REFERRAL_AWARDED: "loyalty.referral.multi_tier_awarded"
48
+ };
49
+ //#endregion
50
+ export { createEvent$1 as n, LoyaltyEvents as t };