@classytic/loyalty 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +191 -0
  3. package/dist/config-aNigy4ec.d.mts +90 -0
  4. package/dist/constants-C75GvizO.mjs +49 -0
  5. package/dist/domain/enums/index.d.mts +2 -0
  6. package/dist/domain/enums/index.mjs +2 -0
  7. package/dist/domain/index.d.mts +90 -0
  8. package/dist/domain/index.mjs +4 -0
  9. package/dist/domain-errors-D7q-2LYD.mjs +120 -0
  10. package/dist/event-emitter.port-CLYPoMni.d.mts +8 -0
  11. package/dist/event-types-xQBtYBbX.mjs +24 -0
  12. package/dist/events/index.d.mts +2 -0
  13. package/dist/events/index.mjs +3 -0
  14. package/dist/events-CprEWlN7.mjs +25 -0
  15. package/dist/index-B6ZstxSP.d.mts +2357 -0
  16. package/dist/index-BcoGeDco.d.mts +127 -0
  17. package/dist/index-CaH4GhSF.d.mts +19 -0
  18. package/dist/index-DqzTjeEB.d.mts +148 -0
  19. package/dist/index-O7TL4R2P.d.mts +86 -0
  20. package/dist/index-_pg4J11t.d.mts +191 -0
  21. package/dist/index.d.mts +21 -0
  22. package/dist/index.mjs +95 -0
  23. package/dist/models/index.d.mts +2 -0
  24. package/dist/models/index.mjs +2 -0
  25. package/dist/models-CrZPmVxt.mjs +417 -0
  26. package/dist/notification.port-BzUsH0It.d.mts +13 -0
  27. package/dist/notifications/index.d.mts +21 -0
  28. package/dist/notifications/index.mjs +19 -0
  29. package/dist/repositories/index.d.mts +2 -0
  30. package/dist/repositories/index.mjs +2 -0
  31. package/dist/repositories-qMX7Xh_y.mjs +440 -0
  32. package/dist/services/index.d.mts +2 -0
  33. package/dist/services/index.mjs +2 -0
  34. package/dist/services-CoTqC4E6.mjs +1098 -0
  35. package/dist/tier-definition-Djf72UPa.d.mts +120 -0
  36. package/dist/tier-definition.port-BzrHYBRo.d.mts +143 -0
  37. package/dist/types/index.d.mts +3 -0
  38. package/dist/types/index.mjs +1 -0
  39. package/package.json +111 -0
package/dist/index.mjs ADDED
@@ -0,0 +1,95 @@
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";
8
+ //#region src/index.ts
9
+ function resolveConfig(config) {
10
+ let tenant;
11
+ if (config.tenant === false) tenant = {
12
+ enabled: false,
13
+ field: "",
14
+ type: "ObjectId",
15
+ contextKey: ""
16
+ };
17
+ else if (config.tenant === void 0) tenant = {
18
+ enabled: true,
19
+ field: DEFAULT_TENANT_FIELD,
20
+ type: DEFAULT_TENANT_TYPE,
21
+ contextKey: DEFAULT_TENANT_FIELD
22
+ };
23
+ else tenant = {
24
+ enabled: true,
25
+ field: config.tenant.field ?? "organizationId",
26
+ type: config.tenant.type ?? "ObjectId",
27
+ contextKey: config.tenant.contextKey ?? config.tenant.field ?? "organizationId",
28
+ ...config.tenant.ref ? { ref: config.tenant.ref } : {}
29
+ };
30
+ const resolved = {
31
+ programId: config.program?.id ?? "default",
32
+ conversionRate: config.program?.conversionRate ?? 10,
33
+ redemption: {
34
+ minPoints: config.redemption?.minPoints ?? 0,
35
+ minOrderAmount: config.redemption?.minOrderAmount ?? 0,
36
+ maxRedeemPercent: config.redemption?.maxRedeemPercent ?? 50,
37
+ reservationTtlMinutes: config.redemption?.reservationTtlMinutes ?? 30
38
+ },
39
+ referral: {
40
+ referrerRewardPoints: config.referral?.referrerRewardPoints ?? 100,
41
+ refereeRewardPoints: config.referral?.refereeRewardPoints ?? 50,
42
+ maxPerPeriod: config.referral?.maxPerPeriod ?? 10,
43
+ periodDays: config.referral?.periodDays ?? 30,
44
+ requireApproval: config.referral?.requireApproval ?? false,
45
+ codeLength: config.referral?.codeLength ?? 8
46
+ },
47
+ expiry: {
48
+ enabled: config.expiry?.enabled ?? false,
49
+ ...config.expiry?.defaultDays !== void 0 ? { defaultDays: config.expiry.defaultDays } : {}
50
+ },
51
+ tenant
52
+ };
53
+ if (resolved.conversionRate <= 0) throw new ValidationError("conversionRate must be positive");
54
+ if (resolved.referral.codeLength < 4) throw new ValidationError("referral codeLength must be >= 4");
55
+ if (resolved.redemption.maxRedeemPercent < 0 || resolved.redemption.maxRedeemPercent > 100) throw new ValidationError("maxRedeemPercent must be between 0 and 100");
56
+ return resolved;
57
+ }
58
+ var MongoUnitOfWork = class {
59
+ constructor(connection) {
60
+ this.connection = connection;
61
+ }
62
+ async withTransaction(cb) {
63
+ const session = await this.connection.startSession();
64
+ try {
65
+ let result;
66
+ await session.withTransaction(async () => {
67
+ result = await cb(session);
68
+ });
69
+ return result;
70
+ } finally {
71
+ await session.endSession();
72
+ }
73
+ }
74
+ };
75
+ function createLoyaltyEngine(config) {
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);
79
+ const unitOfWork = new MongoUnitOfWork(config.mongoose);
80
+ const events = config.events?.adapter ?? new InProcessEventBus();
81
+ return {
82
+ models,
83
+ repositories,
84
+ services: createServices({
85
+ repositories,
86
+ unitOfWork,
87
+ events,
88
+ notifications: config.notifications ?? null,
89
+ config: resolvedConfig
90
+ }),
91
+ events
92
+ };
93
+ }
94
+ //#endregion
95
+ export { LoyaltyEvents, createLoyaltyEngine, resolveConfig };
@@ -0,0 +1,2 @@
1
+ import { n as createModels, t as LoyaltyModels } from "../index-B6ZstxSP.mjs";
2
+ export { LoyaltyModels, createModels };
@@ -0,0 +1,2 @@
1
+ import { t as createModels } from "../models-CrZPmVxt.mjs";
2
+ export { createModels };
@@ -0,0 +1,417 @@
1
+ import { a as MEMBER_STATUSES, c as RULE_STATUSES, l as RULE_TYPES, o as REDEMPTION_STATUSES, s as REFERRAL_STATUSES, u as TRANSACTION_TYPES } from "./constants-C75GvizO.mjs";
2
+ import mongoose from "mongoose";
3
+ //#region src/models/create-model.ts
4
+ /**
5
+ * Injects the tenant field into a schema and prepends it to ALL existing indexes.
6
+ */
7
+ function injectTenantField(schema, tenant) {
8
+ if (!tenant.enabled) return;
9
+ schema.add({ [tenant.field]: {
10
+ type: tenant.type === "ObjectId" ? mongoose.Schema.Types.ObjectId : String,
11
+ required: true,
12
+ index: true,
13
+ ...tenant.ref ? { ref: tenant.ref } : {}
14
+ } });
15
+ const existingIndexes = schema._indexes;
16
+ if (existingIndexes && existingIndexes.length > 0) for (const indexEntry of existingIndexes) {
17
+ const fields = indexEntry[0];
18
+ const newFields = { [tenant.field]: 1 };
19
+ for (const [key, val] of Object.entries(fields)) newFields[key] = val;
20
+ indexEntry[0] = newFields;
21
+ }
22
+ }
23
+ /**
24
+ * Adds user-provided indexes, prepending the tenant field if tenant is enabled.
25
+ */
26
+ function applyUserIndexes(schema, indexes, tenant) {
27
+ for (const def of indexes) {
28
+ const fields = tenant.enabled ? {
29
+ [tenant.field]: 1,
30
+ ...def.fields
31
+ } : { ...def.fields };
32
+ schema.index(fields, def.options ?? {});
33
+ }
34
+ }
35
+ //#endregion
36
+ //#region src/models/schemas/earning-rule.schema.ts
37
+ function createEarningRuleSchema() {
38
+ const schema = new mongoose.Schema({
39
+ programId: {
40
+ type: String,
41
+ default: "default"
42
+ },
43
+ name: {
44
+ type: String,
45
+ required: true
46
+ },
47
+ description: { type: String },
48
+ type: {
49
+ type: String,
50
+ enum: RULE_TYPES,
51
+ required: true
52
+ },
53
+ status: {
54
+ type: String,
55
+ enum: RULE_STATUSES,
56
+ default: "active"
57
+ },
58
+ priority: {
59
+ type: Number,
60
+ default: 0
61
+ },
62
+ conditions: {
63
+ minOrderAmount: { type: Number },
64
+ categories: [{ type: String }],
65
+ tiers: [{ type: String }],
66
+ actions: [{ type: String }],
67
+ dayOfWeek: [{ type: Number }],
68
+ dateRange: {
69
+ start: { type: Date },
70
+ end: { type: Date }
71
+ }
72
+ },
73
+ reward: {
74
+ pointsPerAmount: { type: Number },
75
+ amountPerPoint: { type: Number },
76
+ fixedPoints: { type: Number },
77
+ multiplier: { type: Number },
78
+ roundingMode: {
79
+ type: String,
80
+ enum: [
81
+ "floor",
82
+ "round",
83
+ "ceil"
84
+ ],
85
+ default: "floor"
86
+ },
87
+ maxPointsPerTransaction: { type: Number }
88
+ },
89
+ startsAt: { type: Date },
90
+ endsAt: { type: Date },
91
+ metadata: { type: mongoose.Schema.Types.Mixed }
92
+ }, { timestamps: true });
93
+ schema.index({
94
+ status: 1,
95
+ type: 1,
96
+ priority: 1
97
+ });
98
+ return schema;
99
+ }
100
+ //#endregion
101
+ //#region src/models/schemas/member.schema.ts
102
+ function createMemberSchema() {
103
+ const schema = new mongoose.Schema({
104
+ programId: {
105
+ type: String,
106
+ default: "default"
107
+ },
108
+ externalId: {
109
+ type: String,
110
+ required: true
111
+ },
112
+ externalType: {
113
+ type: String,
114
+ required: true
115
+ },
116
+ status: {
117
+ type: String,
118
+ enum: MEMBER_STATUSES,
119
+ default: "active"
120
+ },
121
+ cardId: {
122
+ type: String,
123
+ unique: true,
124
+ sparse: true
125
+ },
126
+ referralCode: {
127
+ type: String,
128
+ unique: true,
129
+ sparse: true
130
+ },
131
+ tier: {
132
+ type: String,
133
+ default: null
134
+ },
135
+ tierOverride: { type: String },
136
+ tierOverrideReason: { type: String },
137
+ tierQualifiedAt: { type: Date },
138
+ tierExpiresAt: { type: Date },
139
+ balance: {
140
+ current: {
141
+ type: Number,
142
+ default: 0
143
+ },
144
+ lifetime: {
145
+ type: Number,
146
+ default: 0
147
+ },
148
+ redeemed: {
149
+ type: Number,
150
+ default: 0
151
+ },
152
+ expired: {
153
+ type: Number,
154
+ default: 0
155
+ }
156
+ },
157
+ enrolledAt: {
158
+ type: Date,
159
+ default: Date.now
160
+ },
161
+ deactivatedAt: { type: Date },
162
+ metadata: { type: mongoose.Schema.Types.Mixed }
163
+ }, { timestamps: true });
164
+ schema.index({
165
+ externalId: 1,
166
+ externalType: 1
167
+ }, { unique: true });
168
+ return schema;
169
+ }
170
+ //#endregion
171
+ //#region src/models/schemas/point-transaction.schema.ts
172
+ function createPointTransactionSchema() {
173
+ const schema = new mongoose.Schema({
174
+ memberId: {
175
+ type: mongoose.Schema.Types.ObjectId,
176
+ required: true,
177
+ ref: "LoyaltyMember"
178
+ },
179
+ programId: {
180
+ type: String,
181
+ default: "default"
182
+ },
183
+ type: {
184
+ type: String,
185
+ enum: TRANSACTION_TYPES,
186
+ required: true
187
+ },
188
+ points: {
189
+ type: Number,
190
+ required: true
191
+ },
192
+ balanceAfter: {
193
+ type: Number,
194
+ required: true
195
+ },
196
+ description: {
197
+ type: String,
198
+ required: true
199
+ },
200
+ referenceType: { type: String },
201
+ referenceId: { type: String },
202
+ idempotencyKey: { type: String },
203
+ earningRuleId: { type: mongoose.Schema.Types.ObjectId },
204
+ expiresAt: { type: Date },
205
+ expiredAt: { type: Date },
206
+ metadata: { type: mongoose.Schema.Types.Mixed },
207
+ actorId: {
208
+ type: String,
209
+ required: true
210
+ }
211
+ }, { timestamps: {
212
+ createdAt: true,
213
+ updatedAt: false
214
+ } });
215
+ schema.index({
216
+ memberId: 1,
217
+ createdAt: -1
218
+ });
219
+ schema.index({ idempotencyKey: 1 }, {
220
+ unique: true,
221
+ partialFilterExpression: { idempotencyKey: {
222
+ $exists: true,
223
+ $ne: null
224
+ } }
225
+ });
226
+ return schema;
227
+ }
228
+ //#endregion
229
+ //#region src/models/schemas/redemption.schema.ts
230
+ function createRedemptionSchema() {
231
+ const schema = new mongoose.Schema({
232
+ memberId: {
233
+ type: mongoose.Schema.Types.ObjectId,
234
+ required: true,
235
+ ref: "LoyaltyMember"
236
+ },
237
+ programId: {
238
+ type: String,
239
+ default: "default"
240
+ },
241
+ pointsReserved: {
242
+ type: Number,
243
+ required: true
244
+ },
245
+ pointsConfirmed: {
246
+ type: Number,
247
+ default: 0
248
+ },
249
+ discountAmount: {
250
+ type: Number,
251
+ required: true
252
+ },
253
+ conversionRate: {
254
+ type: Number,
255
+ required: true
256
+ },
257
+ status: {
258
+ type: String,
259
+ enum: REDEMPTION_STATUSES,
260
+ default: "reserved"
261
+ },
262
+ ownerType: {
263
+ type: String,
264
+ required: true
265
+ },
266
+ ownerId: {
267
+ type: String,
268
+ required: true
269
+ },
270
+ expiresAt: { type: Date },
271
+ confirmedAt: { type: Date },
272
+ releasedAt: { type: Date },
273
+ transactionId: { type: mongoose.Schema.Types.ObjectId }
274
+ }, { timestamps: true });
275
+ schema.index({
276
+ ownerType: 1,
277
+ ownerId: 1
278
+ }, { unique: true });
279
+ return schema;
280
+ }
281
+ //#endregion
282
+ //#region src/models/schemas/referral.schema.ts
283
+ function createReferralSchema() {
284
+ const schema = new mongoose.Schema({
285
+ programId: {
286
+ type: String,
287
+ default: "default"
288
+ },
289
+ referrerId: {
290
+ type: mongoose.Schema.Types.ObjectId,
291
+ required: true,
292
+ ref: "LoyaltyMember"
293
+ },
294
+ refereeId: {
295
+ type: mongoose.Schema.Types.ObjectId,
296
+ required: true,
297
+ ref: "LoyaltyMember"
298
+ },
299
+ referralCode: {
300
+ type: String,
301
+ required: true
302
+ },
303
+ status: {
304
+ type: String,
305
+ enum: REFERRAL_STATUSES,
306
+ default: "pending"
307
+ },
308
+ referrerRewardPoints: { type: Number },
309
+ refereeRewardPoints: { type: Number },
310
+ referrerTransactionId: { type: mongoose.Schema.Types.ObjectId },
311
+ refereeTransactionId: { type: mongoose.Schema.Types.ObjectId },
312
+ approvedAt: { type: Date },
313
+ rejectedAt: { type: Date },
314
+ rejectionReason: { type: String },
315
+ expiresAt: { type: Date },
316
+ metadata: { type: mongoose.Schema.Types.Mixed }
317
+ }, { timestamps: true });
318
+ schema.index({ refereeId: 1 }, { unique: true });
319
+ schema.index({
320
+ referrerId: 1,
321
+ createdAt: -1
322
+ });
323
+ return schema;
324
+ }
325
+ //#endregion
326
+ //#region src/models/schemas/tier-definition.schema.ts
327
+ function createTierDefinitionSchema() {
328
+ const schema = new mongoose.Schema({
329
+ programId: {
330
+ type: String,
331
+ default: "default"
332
+ },
333
+ name: {
334
+ type: String,
335
+ required: true
336
+ },
337
+ rank: {
338
+ type: Number,
339
+ required: true
340
+ },
341
+ qualificationCriteria: {
342
+ minLifetimePoints: { type: Number },
343
+ minLifetimeSpend: { type: Number },
344
+ evaluationPeriodDays: { type: Number }
345
+ },
346
+ benefits: {
347
+ pointsMultiplier: { type: Number },
348
+ discountPercent: { type: Number },
349
+ freeShipping: { type: Boolean },
350
+ customBenefits: { type: mongoose.Schema.Types.Mixed }
351
+ },
352
+ downgrade: {
353
+ enabled: {
354
+ type: Boolean,
355
+ default: false
356
+ },
357
+ gracePeriodDays: {
358
+ type: Number,
359
+ default: 30
360
+ }
361
+ },
362
+ color: { type: String },
363
+ metadata: { type: mongoose.Schema.Types.Mixed }
364
+ }, { timestamps: true });
365
+ schema.index({
366
+ programId: 1,
367
+ rank: 1
368
+ }, { unique: true });
369
+ schema.index({
370
+ programId: 1,
371
+ name: 1
372
+ }, { unique: true });
373
+ return schema;
374
+ }
375
+ //#endregion
376
+ //#region src/models/index.ts
377
+ function createModels(connection, tenant, indexes) {
378
+ const memberSchema = createMemberSchema();
379
+ const pointTransactionSchema = createPointTransactionSchema();
380
+ const earningRuleSchema = createEarningRuleSchema();
381
+ const tierDefinitionSchema = createTierDefinitionSchema();
382
+ const redemptionSchema = createRedemptionSchema();
383
+ const referralSchema = createReferralSchema();
384
+ if (tenant.enabled) {
385
+ injectTenantField(memberSchema, tenant);
386
+ injectTenantField(pointTransactionSchema, tenant);
387
+ injectTenantField(earningRuleSchema, tenant);
388
+ injectTenantField(tierDefinitionSchema, tenant);
389
+ injectTenantField(redemptionSchema, tenant);
390
+ injectTenantField(referralSchema, tenant);
391
+ }
392
+ if (indexes) {
393
+ const schemaMap = {
394
+ member: memberSchema,
395
+ pointTransaction: pointTransactionSchema,
396
+ earningRule: earningRuleSchema,
397
+ tierDefinition: tierDefinitionSchema,
398
+ redemption: redemptionSchema,
399
+ referral: referralSchema
400
+ };
401
+ for (const [modelName, defs] of Object.entries(indexes)) {
402
+ const schema = schemaMap[modelName];
403
+ if (schema && defs) applyUserIndexes(schema, defs, tenant);
404
+ }
405
+ }
406
+ const model = (name, schema) => connection.models[name] || connection.model(name, schema);
407
+ return {
408
+ Member: model("LoyaltyMember", memberSchema),
409
+ PointTransaction: model("LoyaltyPointTransaction", pointTransactionSchema),
410
+ EarningRule: model("LoyaltyEarningRule", earningRuleSchema),
411
+ TierDefinition: model("LoyaltyTierDefinition", tierDefinitionSchema),
412
+ Redemption: model("LoyaltyRedemption", redemptionSchema),
413
+ Referral: model("LoyaltyReferral", referralSchema)
414
+ };
415
+ }
416
+ //#endregion
417
+ export { createModels as t };
@@ -0,0 +1,13 @@
1
+ //#region src/domain/ports/notification.port.d.ts
2
+ /**
3
+ * Optional notification adapter. Implement to receive loyalty events
4
+ * and dispatch notifications through your own system (email, SMS, push, in-app).
5
+ *
6
+ * The loyalty engine calls this adapter after every state change.
7
+ * The adapter decides what to send, how, and to whom.
8
+ */
9
+ interface NotificationAdapter {
10
+ notify(event: string, payload: Record<string, unknown>): void | Promise<void>;
11
+ }
12
+ //#endregion
13
+ export { NotificationAdapter as t };
@@ -0,0 +1,21 @@
1
+ import { t as NotificationAdapter } from "../notification.port-BzUsH0It.mjs";
2
+
3
+ //#region src/notifications/index.d.ts
4
+ interface NotificationsAdapterConfig {
5
+ service: {
6
+ send: (payload: unknown) => Promise<unknown>;
7
+ };
8
+ resolveRecipient: (payload: Record<string, unknown>) => Promise<{
9
+ id?: string;
10
+ email?: string;
11
+ phone?: string;
12
+ name?: string;
13
+ deviceToken?: string;
14
+ } | null>;
15
+ templates?: Record<string, string>;
16
+ channels?: Record<string, string[]>;
17
+ skip?: string[];
18
+ }
19
+ declare function createNotificationsAdapter(config: NotificationsAdapterConfig): NotificationAdapter;
20
+ //#endregion
21
+ export { NotificationsAdapterConfig, createNotificationsAdapter };
@@ -0,0 +1,19 @@
1
+ //#region src/notifications/index.ts
2
+ function createNotificationsAdapter(config) {
3
+ return { async notify(event, payload) {
4
+ if (config.skip?.includes(event)) return;
5
+ const template = config.templates?.[event];
6
+ if (config.templates && !template) return;
7
+ const recipient = await config.resolveRecipient(payload);
8
+ if (!recipient) return;
9
+ await config.service.send({
10
+ event,
11
+ recipient,
12
+ data: payload,
13
+ template,
14
+ channels: config.channels?.[event]
15
+ });
16
+ } };
17
+ }
18
+ //#endregion
19
+ export { createNotificationsAdapter };
@@ -0,0 +1,2 @@
1
+ import { a as RedemptionRepository, c as EarningRuleRepository, i as ReferralRepository, n as createRepositories, o as PointTransactionRepository, r as TierDefinitionRepository, s as MemberRepository, t as LoyaltyRepositories } from "../index-BcoGeDco.mjs";
2
+ export { EarningRuleRepository, LoyaltyRepositories, MemberRepository, PointTransactionRepository, RedemptionRepository, ReferralRepository, TierDefinitionRepository, createRepositories };
@@ -0,0 +1,2 @@
1
+ import { a as PointTransactionRepository, i as RedemptionRepository, n as TierDefinitionRepository, o as MemberRepository, r as ReferralRepository, s as EarningRuleRepository, t as createRepositories } from "../repositories-qMX7Xh_y.mjs";
2
+ export { EarningRuleRepository, MemberRepository, PointTransactionRepository, RedemptionRepository, ReferralRepository, TierDefinitionRepository, createRepositories };