@classytic/loyalty 0.1.0 → 0.2.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.
- package/CHANGELOG.md +68 -0
- package/dist/config-JYTMP1F7.d.mts +136 -0
- package/dist/{constants-C75GvizO.mjs → constants-DkQYkOK1.mjs} +3 -2
- package/dist/domain/enums/index.d.mts +1 -1
- package/dist/domain/enums/index.mjs +1 -1
- package/dist/events/index.d.mts +2 -2
- package/dist/events/index.mjs +3 -3
- package/dist/events-BjzHSLBn.mjs +344 -0
- package/dist/index-2izQQext.d.mts +281 -0
- package/dist/index-BmqGuj38.d.mts +400 -0
- package/dist/{index-CaH4GhSF.d.mts → index-DMK8a98J.d.mts} +1 -1
- package/dist/index-qI1WvtNg.d.mts +361 -0
- package/dist/index.d.mts +28 -12
- package/dist/index.mjs +30 -26
- package/dist/loyalty-events-Cyt9TwX6.mjs +50 -0
- package/dist/models/index.d.mts +1225 -2
- package/dist/models/index.mjs +2 -2
- package/dist/{models-CrZPmVxt.mjs → models-uhh9c-7k.mjs} +104 -41
- package/dist/notifications/index.d.mts +1 -1
- package/dist/repositories/index.d.mts +2 -2
- package/dist/repositories/index.mjs +1 -1
- package/dist/repositories-ObGUgcSH.mjs +1917 -0
- package/dist/types/index.d.mts +2 -2
- package/package.json +18 -16
- package/dist/config-aNigy4ec.d.mts +0 -90
- package/dist/domain/index.d.mts +0 -90
- package/dist/domain/index.mjs +0 -4
- package/dist/domain-errors-D7q-2LYD.mjs +0 -120
- package/dist/event-emitter.port-CLYPoMni.d.mts +0 -8
- package/dist/event-types-xQBtYBbX.mjs +0 -24
- package/dist/events-CprEWlN7.mjs +0 -25
- package/dist/index-B6ZstxSP.d.mts +0 -2357
- package/dist/index-BcoGeDco.d.mts +0 -127
- package/dist/index-DqzTjeEB.d.mts +0 -148
- package/dist/index-O7TL4R2P.d.mts +0 -86
- package/dist/index-_pg4J11t.d.mts +0 -191
- package/dist/repositories-qMX7Xh_y.mjs +0 -440
- package/dist/services/index.d.mts +0 -2
- package/dist/services/index.mjs +0 -2
- package/dist/services-CoTqC4E6.mjs +0 -1098
- package/dist/tier-definition-Djf72UPa.d.mts +0 -120
- package/dist/tier-definition.port-BzrHYBRo.d.mts +0 -143
- /package/dist/{notification.port-BzUsH0It.d.mts → notification.port-9B5xlzxQ.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-2izQQext.mjs";
|
|
3
|
+
import { t as NotificationAdapter } from "./notification.port-9B5xlzxQ.mjs";
|
|
4
|
+
import { c as ResolvedTenant, o as RepositoryPlugins, s as ResolvedConfig } from "./config-JYTMP1F7.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 };
|
package/dist/index.d.mts
CHANGED
|
@@ -1,21 +1,37 @@
|
|
|
1
|
-
import { a as
|
|
2
|
-
import { l as
|
|
3
|
-
import { t as
|
|
4
|
-
import { n as
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
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 { A as TierUpgraded, B as PointsEarnedPayload, C as ReferralRecorded, D as TierGraceStarted, E as TierDowngraded, F as LoyaltyEventPayload, G as TierChangedPayload, H as RedemptionPayload, I as LoyaltyEvents, J as InProcessLoyaltyBus, L as MemberLifecyclePayload, N as LoyaltyEventMap, P as LoyaltyEventName, S as ReferralApproved, T as ReferralRewarded, V as PointsExpiredPayload, W as ReferralPayload, Y as InProcessLoyaltyBusOptions, _ as PointsRedeemed, a as LoyaltyEventDefinition, b as RedemptionReserved, c as MemberDeactivated, d as MemberSuspended, g as PointsExpiringSoon, h as PointsExpired, j as loyaltyEventDefinitions, l as MemberEnrolled, m as PointsEarned, o as LoyaltyEventPayloadOf, p as PointsAdjusted, q as createEvent, s as LoyaltyEventSchema, u as MemberReactivated, v as RedemptionConfirmed, w as ReferralRejected, y as RedemptionReleased, z as PointsAdjustedPayload } from "./index-BmqGuj38.mjs";
|
|
2
|
+
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-2izQQext.mjs";
|
|
3
|
+
import { t as NotificationAdapter } from "./notification.port-9B5xlzxQ.mjs";
|
|
4
|
+
import { c as ResolvedTenant, n as LoyaltyConfig, r as LoyaltyModelName, s as ResolvedConfig } from "./config-JYTMP1F7.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-qI1WvtNg.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
|
-
|
|
17
|
-
|
|
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
|
|
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-
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import { t as
|
|
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-ObGUgcSH.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-BjzHSLBn.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
|
-
|
|
14
|
-
|
|
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
|
-
|
|
20
|
-
|
|
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
|
-
|
|
26
|
-
|
|
27
|
-
contextKey: config.tenant.contextKey ?? config.tenant.
|
|
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.
|
|
79
|
+
const events = config.eventTransport ?? new InProcessLoyaltyBus();
|
|
81
80
|
return {
|
|
82
81
|
models,
|
|
83
|
-
repositories
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
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
|
-
|
|
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 };
|