@classytic/loyalty 0.7.0 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +134 -0
- package/dist/constants-DfrENxFG.mjs +75 -0
- package/dist/domain/earn-base.d.mts +35 -0
- package/dist/domain/earn-base.mjs +37 -0
- package/dist/domain/enums/index.d.mts +2 -2
- package/dist/domain/enums/index.mjs +1 -2
- package/dist/domain/rule-schedule.d.mts +24 -0
- package/dist/domain/rule-schedule.mjs +12 -0
- package/dist/events/index.d.mts +2 -2
- package/dist/events/index.mjs +3 -3
- package/dist/{events-BLjg3uLA.mjs → events-DhZolW61.mjs} +104 -4
- package/dist/{index-BIPHUsfR.d.mts → index-BvyzKU7R.d.mts} +283 -34
- package/dist/{index-BIhVuwIZ.d.mts → index-CorB0JL6.d.mts} +255 -109
- package/dist/index-DRrEhE0r.d.mts +604 -0
- package/dist/{index-YUFamhDk.d.mts → index-Db5Gb7x_.d.mts} +187 -39
- package/dist/index-tjMMmPzU.d.mts +36 -0
- package/dist/index.d.mts +121 -30
- package/dist/index.mjs +59 -10
- package/dist/{loyalty-events-DXirolM_.mjs → loyalty-events-j29B66YW.mjs} +20 -13
- package/dist/models/index.d.mts +2 -2
- package/dist/models/index.mjs +1 -2
- package/dist/{models-weWqS5Bc.mjs → models-BEGLduz1.mjs} +276 -7
- package/dist/notifications/index.d.mts +3 -4
- package/dist/repositories/index.d.mts +2 -2
- package/dist/repositories/index.mjs +1 -2
- package/dist/{repositories-DErtHQou.mjs → repositories-ER68JmUo.mjs} +1161 -99
- package/dist/types/index.d.mts +2 -3
- package/package.json +39 -22
- package/dist/config-CtkGj3xZ.d.mts +0 -178
- package/dist/constants-DOPBh6kt.mjs +0 -48
- package/dist/index-Cp_sXNpG.d.mts +0 -349
- package/dist/index-D_Oe3aDk.d.mts +0 -19
|
@@ -0,0 +1,604 @@
|
|
|
1
|
+
import { f as TransactionType, o as RuleStatus, r as MemberStatus, s as RuleType, t as EarnSource } from "./index-tjMMmPzU.mjs";
|
|
2
|
+
import { t as NotificationAdapter } from "./notification.port-wY4IKimz.mjs";
|
|
3
|
+
import { ResolvedTenantConfig, TenantConfig } from "@classytic/repo-core/tenant";
|
|
4
|
+
import { EventTransport } from "@classytic/primitives/events";
|
|
5
|
+
import { OperationContext } from "@classytic/primitives/context";
|
|
6
|
+
import { OutboxStore } from "@classytic/primitives/outbox";
|
|
7
|
+
import { ApprovalChain } from "@classytic/primitives/approval";
|
|
8
|
+
//#region src/domain/entities/earning-rule.d.ts
|
|
9
|
+
interface EarningConditions {
|
|
10
|
+
/**
|
|
11
|
+
* Minimum order total for the rule to apply — **integer minor units** of the
|
|
12
|
+
* host's ambient currency (paisa for BDT). `10000` = ৳100. Compared against
|
|
13
|
+
* `evaluateOrder`'s `orderTotal` (also minor units).
|
|
14
|
+
*/
|
|
15
|
+
minOrderAmount?: number;
|
|
16
|
+
categories?: string[];
|
|
17
|
+
tiers?: string[];
|
|
18
|
+
actions?: string[];
|
|
19
|
+
/**
|
|
20
|
+
* Days the rule applies, JS `getDay()` convention: 0=Sun … 6=Sat. Empty /
|
|
21
|
+
* omitted = every day. Evaluated against the wall-clock weekday in the
|
|
22
|
+
* engine's configured `timezone` (UTC when unset), never the server's local
|
|
23
|
+
* clock. See `LoyaltyShape.timezone`.
|
|
24
|
+
*/
|
|
25
|
+
dayOfWeek?: number[];
|
|
26
|
+
dateRange?: {
|
|
27
|
+
start?: Date;
|
|
28
|
+
end?: Date;
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
interface EarningReward {
|
|
32
|
+
/**
|
|
33
|
+
* Points awarded per `rateBasis` minor units of spend:
|
|
34
|
+
* `points = pointsPerAmount × (orderTotalMinor / rateBasis)` (rounded once
|
|
35
|
+
* per `roundingMode`). REQUIRES `rateBasis` — there is no implicit
|
|
36
|
+
* "per 1 major unit" basis anymore (that ambiguity was the bug).
|
|
37
|
+
* "5 points per ৳100" = `{ pointsPerAmount: 5, rateBasis: 10000 }`.
|
|
38
|
+
*/
|
|
39
|
+
pointsPerAmount?: number;
|
|
40
|
+
/**
|
|
41
|
+
* RATE: the explicit integer minor-unit basis `pointsPerAmount` is quoted
|
|
42
|
+
* against (≥ 1). Required whenever `pointsPerAmount` is set.
|
|
43
|
+
*/
|
|
44
|
+
rateBasis?: number;
|
|
45
|
+
/**
|
|
46
|
+
* RATE: integer minor units of spend per single point (≥ 1).
|
|
47
|
+
* `points = orderTotalMinor / amountPerPoint` (rounded per `roundingMode`).
|
|
48
|
+
* "1 point per ৳100" = `amountPerPoint: 10000`.
|
|
49
|
+
*/
|
|
50
|
+
amountPerPoint?: number;
|
|
51
|
+
fixedPoints?: number;
|
|
52
|
+
multiplier?: number;
|
|
53
|
+
roundingMode?: 'floor' | 'round' | 'ceil';
|
|
54
|
+
maxPointsPerTransaction?: number;
|
|
55
|
+
}
|
|
56
|
+
interface EarningRule {
|
|
57
|
+
_id: string;
|
|
58
|
+
programId: string;
|
|
59
|
+
name: string;
|
|
60
|
+
description?: string;
|
|
61
|
+
type: RuleType;
|
|
62
|
+
status: RuleStatus;
|
|
63
|
+
priority: number;
|
|
64
|
+
conditions: EarningConditions;
|
|
65
|
+
reward: EarningReward;
|
|
66
|
+
startsAt?: Date;
|
|
67
|
+
endsAt?: Date;
|
|
68
|
+
metadata?: Record<string, unknown>;
|
|
69
|
+
createdAt: Date;
|
|
70
|
+
updatedAt: Date;
|
|
71
|
+
}
|
|
72
|
+
//#endregion
|
|
73
|
+
//#region src/domain/entities/tier-definition.d.ts
|
|
74
|
+
interface TierQualification {
|
|
75
|
+
minLifetimePoints?: number;
|
|
76
|
+
/**
|
|
77
|
+
* Minimum lifetime spend — **integer minor units** (paisa for BDT).
|
|
78
|
+
* Declared for host-side qualification logic only: the in-package tier
|
|
79
|
+
* evaluation checks lifetime POINTS exclusively and never reads this field.
|
|
80
|
+
*/
|
|
81
|
+
minLifetimeSpend?: number;
|
|
82
|
+
evaluationPeriodDays?: number;
|
|
83
|
+
}
|
|
84
|
+
interface TierBenefits {
|
|
85
|
+
pointsMultiplier?: number;
|
|
86
|
+
discountPercent?: number;
|
|
87
|
+
freeShipping?: boolean;
|
|
88
|
+
customBenefits?: Record<string, unknown>;
|
|
89
|
+
}
|
|
90
|
+
interface TierDowngrade {
|
|
91
|
+
enabled: boolean;
|
|
92
|
+
gracePeriodDays: number;
|
|
93
|
+
}
|
|
94
|
+
interface TierDefinition {
|
|
95
|
+
_id: string;
|
|
96
|
+
programId: string;
|
|
97
|
+
name: string;
|
|
98
|
+
rank: number;
|
|
99
|
+
qualificationCriteria: TierQualification;
|
|
100
|
+
benefits: TierBenefits;
|
|
101
|
+
downgrade: TierDowngrade;
|
|
102
|
+
color?: string;
|
|
103
|
+
metadata?: Record<string, unknown>;
|
|
104
|
+
createdAt: Date;
|
|
105
|
+
updatedAt: Date;
|
|
106
|
+
}
|
|
107
|
+
//#endregion
|
|
108
|
+
//#region src/domain/ports/reward-issuer.port.d.ts
|
|
109
|
+
interface StampVoucherRequest {
|
|
110
|
+
voucherProgramId: string;
|
|
111
|
+
stampProgramId: string;
|
|
112
|
+
memberId: string;
|
|
113
|
+
/** The member's external subject — what the host keys its customers by. */
|
|
114
|
+
externalId: string;
|
|
115
|
+
externalType: string;
|
|
116
|
+
/** Deterministic per award: the SAME key must return the SAME voucher, however often it is retried. */
|
|
117
|
+
awardKey: string;
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Issues a stamp card's non-points reward. Host-supplied (`LoyaltyRuntime.rewardIssuer`) — loyalty
|
|
121
|
+
* never imports a promo engine. Required by any program whose reward is a voucher: such a program
|
|
122
|
+
* is refused at write when none is bound.
|
|
123
|
+
*/
|
|
124
|
+
interface StampRewardIssuer {
|
|
125
|
+
issueVoucher(request: StampVoucherRequest, ctx: LoyaltyContext): Promise<{
|
|
126
|
+
voucherId: string;
|
|
127
|
+
code: string;
|
|
128
|
+
}>;
|
|
129
|
+
}
|
|
130
|
+
//#endregion
|
|
131
|
+
//#region src/types/config.d.ts
|
|
132
|
+
type LoyaltyModelName = 'Member' | 'PointTransaction' | 'EarningRule' | 'TierDefinition' | 'Redemption' | 'Referral' | 'StampProgram' | 'StampEntry' | 'StampAward';
|
|
133
|
+
type TenantConfig$1 = TenantConfig | boolean;
|
|
134
|
+
/** @deprecated Use `ResolvedTenantConfig` from `@classytic/repo-core/tenant`. */
|
|
135
|
+
type ResolvedTenant = ResolvedTenantConfig;
|
|
136
|
+
interface IndexDefinition {
|
|
137
|
+
fields: Record<string, 1 | -1>;
|
|
138
|
+
options?: Record<string, unknown> | undefined;
|
|
139
|
+
}
|
|
140
|
+
type ModelName = 'member' | 'pointTransaction' | 'earningRule' | 'tierDefinition' | 'redemption' | 'referral' | 'stampProgram' | 'stampEntry' | 'stampAward';
|
|
141
|
+
type PluginType = unknown;
|
|
142
|
+
interface RepositoryPlugins {
|
|
143
|
+
member?: PluginType[] | undefined;
|
|
144
|
+
pointTransaction?: PluginType[] | undefined;
|
|
145
|
+
earningRule?: PluginType[] | undefined;
|
|
146
|
+
tierDefinition?: PluginType[] | undefined;
|
|
147
|
+
redemption?: PluginType[] | undefined;
|
|
148
|
+
referral?: PluginType[] | undefined;
|
|
149
|
+
stampProgram?: PluginType[] | undefined;
|
|
150
|
+
stampEntry?: PluginType[] | undefined;
|
|
151
|
+
stampAward?: PluginType[] | undefined;
|
|
152
|
+
}
|
|
153
|
+
interface LoyaltyShape {
|
|
154
|
+
tenant?: TenantConfig$1 | undefined;
|
|
155
|
+
/**
|
|
156
|
+
* IANA timezone name (e.g. `'Asia/Dhaka'`, `'America/New_York'`) used to
|
|
157
|
+
* evaluate wall-clock rule conditions — currently `EarningConditions.dayOfWeek`
|
|
158
|
+
* ("2× points on Fridays"). Resolved DST-exact per instant via
|
|
159
|
+
* `@classytic/primitives/timezone`. Validated at boot: an unknown zone throws
|
|
160
|
+
* `ValidationError`, never a silent mis-evaluation.
|
|
161
|
+
*
|
|
162
|
+
* Omitted → **UTC** (the day boundary is the UTC calendar day). Either way the
|
|
163
|
+
* result NEVER depends on the deploy machine's `TZ` env. PACKAGE_RULES P12.
|
|
164
|
+
*/
|
|
165
|
+
timezone?: string | undefined;
|
|
166
|
+
program?: {
|
|
167
|
+
id?: string | undefined;
|
|
168
|
+
/**
|
|
169
|
+
* RATE: integer minor units ONE point redeems for (≥ 1) in the host's
|
|
170
|
+
* ambient currency. `pointValueMinor: 10` = 1 point → 10 paisa
|
|
171
|
+
* (৳0.10, i.e. the old `conversionRate: 10` "10 points per ৳1").
|
|
172
|
+
* Replaces `conversionRate` (0.5.x), whose implicit per-1-MAJOR-unit
|
|
173
|
+
* basis was ambiguous.
|
|
174
|
+
*/
|
|
175
|
+
pointValueMinor?: number | undefined;
|
|
176
|
+
} | undefined;
|
|
177
|
+
redemption?: {
|
|
178
|
+
minPoints?: number | undefined;
|
|
179
|
+
/**
|
|
180
|
+
* Minimum order total eligible for redemption — **integer minor
|
|
181
|
+
* units** (paisa for BDT). Compared against `validate()`'s
|
|
182
|
+
* `orderTotal` (also minor units).
|
|
183
|
+
*/
|
|
184
|
+
minOrderAmount?: number | undefined;
|
|
185
|
+
maxRedeemPercent?: number | undefined;
|
|
186
|
+
reservationTtlMinutes?: number | undefined;
|
|
187
|
+
} | undefined;
|
|
188
|
+
referral?: {
|
|
189
|
+
referrerRewardPoints?: number | undefined;
|
|
190
|
+
refereeRewardPoints?: number | undefined;
|
|
191
|
+
maxPerPeriod?: number | undefined;
|
|
192
|
+
periodDays?: number | undefined;
|
|
193
|
+
requireApproval?: boolean | undefined;
|
|
194
|
+
codeLength?: number | undefined;
|
|
195
|
+
/**
|
|
196
|
+
* Maximum ancestor depth for multi-tier referral rewards. When >= 2,
|
|
197
|
+
* the direct referrer gets `tierRates[0]` percent of
|
|
198
|
+
* `referrerRewardPoints`, the 2nd-tier ancestor gets `tierRates[1]`,
|
|
199
|
+
* etc. Depth 1 = direct referrer (matches single-hop legacy path).
|
|
200
|
+
* Default: 1 (single-hop, preserves prior behaviour).
|
|
201
|
+
*/
|
|
202
|
+
maxDepth?: number | undefined;
|
|
203
|
+
/**
|
|
204
|
+
* Percent rates (0–100) per depth. `tierRates[0]` is the direct
|
|
205
|
+
* referrer's share. Omitted depths fall back to 0. Example:
|
|
206
|
+
* `{ maxDepth: 3, tierRates: [100, 50, 25] }` — direct gets 100%,
|
|
207
|
+
* grand-referrer 50%, great-grand 25%.
|
|
208
|
+
*/
|
|
209
|
+
tierRates?: number[] | undefined;
|
|
210
|
+
} | undefined;
|
|
211
|
+
expiry?: {
|
|
212
|
+
enabled?: boolean | undefined;
|
|
213
|
+
defaultDays?: number | undefined;
|
|
214
|
+
} | undefined;
|
|
215
|
+
indexes?: Partial<Record<ModelName, IndexDefinition[]>> | undefined;
|
|
216
|
+
autoIndex?: boolean | Partial<Record<LoyaltyModelName, boolean>> | undefined;
|
|
217
|
+
/**
|
|
218
|
+
* Optional prefix prepended to every physical collection this package
|
|
219
|
+
* creates (see PACKAGE_RULES.md §20.1). Unset → default names
|
|
220
|
+
* (`loyalty_members`, `loyalty_point_transactions`, `loyalty_earning_rules`,
|
|
221
|
+
* `loyalty_tier_definitions`, `loyalty_redemptions`, `loyalty_referrals`).
|
|
222
|
+
* Model names and `ref:` populate are unaffected.
|
|
223
|
+
*/
|
|
224
|
+
collectionPrefix?: string | undefined;
|
|
225
|
+
/**
|
|
226
|
+
* When true, existing Mongoose models with loyalty's names are deleted
|
|
227
|
+
* from the connection before re-registering. Hot-reload / test fixtures
|
|
228
|
+
* only. Default `false` — collision throws mongokit's `ModelCollisionError`.
|
|
229
|
+
* Hosts that need two loyalty engines should use two Mongoose connections
|
|
230
|
+
* (`mongoose.createConnection(...)`). See PACKAGE_RULES.md §21.
|
|
231
|
+
*/
|
|
232
|
+
forceRecreate?: boolean | undefined;
|
|
233
|
+
}
|
|
234
|
+
/** Settings an operator changes while the program runs. Unset = the shape's value. */
|
|
235
|
+
interface LiveProgramSettings {
|
|
236
|
+
pointValueMinor?: number | undefined;
|
|
237
|
+
redemption?: {
|
|
238
|
+
minPoints?: number | undefined;
|
|
239
|
+
minOrderAmount?: number | undefined;
|
|
240
|
+
maxRedeemPercent?: number | undefined;
|
|
241
|
+
} | undefined;
|
|
242
|
+
/** Days until earned points expire; `null` switches expiry off. */
|
|
243
|
+
expiryDays?: number | null | undefined;
|
|
244
|
+
}
|
|
245
|
+
interface LoyaltyRuntime {
|
|
246
|
+
/**
|
|
247
|
+
* Arc-compatible event transport. When omitted, the engine creates (and
|
|
248
|
+
* OWNS, so `close()` closes) an `InProcessLoyaltyBus` — a ~60-line structural
|
|
249
|
+
* match of arc's `MemoryEventTransport`. A host-supplied transport is the
|
|
250
|
+
* host's to close. See PACKAGE_RULES §13.
|
|
251
|
+
*/
|
|
252
|
+
eventTransport?: EventTransport | undefined;
|
|
253
|
+
/**
|
|
254
|
+
* Host-owned outbox store (PACKAGE_RULES §5.5 + P8). When present, every
|
|
255
|
+
* domain event is persisted via `outbox.save(event, { session })` **before**
|
|
256
|
+
* `eventTransport.publish(event)`. `session` is threaded from `ctx.session`
|
|
257
|
+
* so the outbox row commits atomically with the business write.
|
|
258
|
+
*
|
|
259
|
+
* Loyalty does NOT ship a store. Hosts wire arc's `repositoryAsOutboxStore(mongokitRepo)`
|
|
260
|
+
* over their own `event_outbox` collection, arc's `MemoryOutboxStore`, or any
|
|
261
|
+
* custom Postgres / Redis / Kafka implementation of the contract. When
|
|
262
|
+
* omitted, events flow through `eventTransport` only (no durable log).
|
|
263
|
+
*/
|
|
264
|
+
outbox?: OutboxStore | undefined;
|
|
265
|
+
notifications?: NotificationAdapter | undefined;
|
|
266
|
+
plugins?: RepositoryPlugins | undefined;
|
|
267
|
+
/**
|
|
268
|
+
* The operator-owned program settings, read PER OPERATION. Supply it when an
|
|
269
|
+
* admin can change them while the program runs; the shape's values are the
|
|
270
|
+
* fallback for anything it leaves unset. Without it they are fixed at bind,
|
|
271
|
+
* so a saved change does nothing until restart.
|
|
272
|
+
*/
|
|
273
|
+
programSettings?: (() => LiveProgramSettings | Promise<LiveProgramSettings>) | undefined;
|
|
274
|
+
/**
|
|
275
|
+
* Skip the boot-time capability gate that asserts the backend supports
|
|
276
|
+
* multi-document `transactions`. Loyalty's point-mutation verbs (earn,
|
|
277
|
+
* adjust, reserve, confirm, release, reverse, processExpirations) run inside
|
|
278
|
+
* `withTransaction`; without a replica set those writes are NOT atomic.
|
|
279
|
+
* Leave `false` (default) in production — set `true` only for local-dev on
|
|
280
|
+
* standalone Mongo, accepting partial-write risk. See PACKAGE_RULES
|
|
281
|
+
* "Runtime capabilities".
|
|
282
|
+
*/
|
|
283
|
+
allowNonTransactional?: boolean | undefined;
|
|
284
|
+
/**
|
|
285
|
+
* Issues stamp-card voucher rewards. Required by any stamp program whose reward is a voucher —
|
|
286
|
+
* creating one on an engine bound without it is refused.
|
|
287
|
+
*/
|
|
288
|
+
rewardIssuer?: StampRewardIssuer | undefined;
|
|
289
|
+
/**
|
|
290
|
+
* Where the engine's boot warnings go — today, the `allowNonTransactional`
|
|
291
|
+
* downgrade notice. Defaults to `console`. A downgrade that carries
|
|
292
|
+
* partial-write risk on stored value must be VISIBLE, and a host that routes
|
|
293
|
+
* logs elsewhere would otherwise never see it.
|
|
294
|
+
*/
|
|
295
|
+
logger?: {
|
|
296
|
+
warn?(message: string, ...args: unknown[]): void;
|
|
297
|
+
error?(message: string, ...args: unknown[]): void;
|
|
298
|
+
} | undefined;
|
|
299
|
+
}
|
|
300
|
+
interface ResolvedConfig {
|
|
301
|
+
programId: string;
|
|
302
|
+
/** Integer minor units one point redeems for (≥ 1). See `LoyaltyShape.program.pointValueMinor`. */
|
|
303
|
+
pointValueMinor: number;
|
|
304
|
+
redemption: {
|
|
305
|
+
minPoints: number;
|
|
306
|
+
/** Integer minor units. */
|
|
307
|
+
minOrderAmount: number;
|
|
308
|
+
maxRedeemPercent: number;
|
|
309
|
+
reservationTtlMinutes: number;
|
|
310
|
+
};
|
|
311
|
+
referral: {
|
|
312
|
+
referrerRewardPoints: number;
|
|
313
|
+
refereeRewardPoints: number;
|
|
314
|
+
maxPerPeriod: number;
|
|
315
|
+
periodDays: number;
|
|
316
|
+
requireApproval: boolean;
|
|
317
|
+
codeLength: number;
|
|
318
|
+
maxDepth: number;
|
|
319
|
+
tierRates: number[];
|
|
320
|
+
};
|
|
321
|
+
expiry: {
|
|
322
|
+
enabled: boolean;
|
|
323
|
+
defaultDays?: number;
|
|
324
|
+
};
|
|
325
|
+
/** Resolved IANA zone for wall-clock rule evaluation; `undefined` = UTC. */
|
|
326
|
+
timezone?: string;
|
|
327
|
+
tenant: ResolvedTenantConfig;
|
|
328
|
+
}
|
|
329
|
+
//#endregion
|
|
330
|
+
//#region src/domain/entities/member.d.ts
|
|
331
|
+
interface MemberBalance {
|
|
332
|
+
current: number;
|
|
333
|
+
lifetime: number;
|
|
334
|
+
redeemed: number;
|
|
335
|
+
expired: number;
|
|
336
|
+
}
|
|
337
|
+
interface Member {
|
|
338
|
+
_id: string;
|
|
339
|
+
programId: string;
|
|
340
|
+
externalId: string;
|
|
341
|
+
externalType: string;
|
|
342
|
+
status: MemberStatus;
|
|
343
|
+
/** Public loyalty card identifier (e.g., MBR-DHK-00004821-8). Set by the integration layer. */
|
|
344
|
+
cardId?: string;
|
|
345
|
+
/** Referral code for the referral system (separate from cardId). Auto-generated on enrollment. */
|
|
346
|
+
referralCode: string;
|
|
347
|
+
tier: string | null;
|
|
348
|
+
tierOverride: string | null;
|
|
349
|
+
tierOverrideReason?: string;
|
|
350
|
+
/**
|
|
351
|
+
* Optional TTL on a manual tier override. When set, `evaluateMember`
|
|
352
|
+
* treats the override as already cleared once `tierOverrideExpiresAt <=
|
|
353
|
+
* now`, and a host-scheduled `sweepExpiredOverrides` cron clears the
|
|
354
|
+
* field + emits a `TIER_OVERRIDE_EXPIRED` audit event.
|
|
355
|
+
*/
|
|
356
|
+
tierOverrideExpiresAt?: Date;
|
|
357
|
+
/** Optional upstream referrer for multi-tier referral chains. */
|
|
358
|
+
referredBy?: string;
|
|
359
|
+
tierQualifiedAt?: Date;
|
|
360
|
+
tierExpiresAt?: Date;
|
|
361
|
+
balance: MemberBalance;
|
|
362
|
+
enrolledAt: Date;
|
|
363
|
+
deactivatedAt?: Date;
|
|
364
|
+
metadata?: Record<string, unknown>;
|
|
365
|
+
createdAt: Date;
|
|
366
|
+
updatedAt: Date;
|
|
367
|
+
}
|
|
368
|
+
//#endregion
|
|
369
|
+
//#region src/domain/entities/point-transaction.d.ts
|
|
370
|
+
interface PointTransaction {
|
|
371
|
+
_id: string;
|
|
372
|
+
memberId: string;
|
|
373
|
+
programId: string;
|
|
374
|
+
type: TransactionType;
|
|
375
|
+
points: number;
|
|
376
|
+
balanceAfter: number;
|
|
377
|
+
description: string;
|
|
378
|
+
/** Earn rows: why the points were granted. */
|
|
379
|
+
source?: EarnSource;
|
|
380
|
+
/** Minor units one point was worth when this row was written. Absent on rows written before it existed. */
|
|
381
|
+
pointValueMinor?: number;
|
|
382
|
+
referenceType?: string;
|
|
383
|
+
referenceId?: string;
|
|
384
|
+
idempotencyKey?: string;
|
|
385
|
+
earningRuleId?: string;
|
|
386
|
+
expiresAt?: Date;
|
|
387
|
+
expiredAt?: Date;
|
|
388
|
+
metadata?: Record<string, unknown>;
|
|
389
|
+
actorId: string;
|
|
390
|
+
/**
|
|
391
|
+
* Optional multi-step approval chain (P7 canonical — same shape as
|
|
392
|
+
* `@classytic/purchase` / `@classytic/order`). Manual `adjustment` /
|
|
393
|
+
* `bonus` transactions above a host-defined threshold gate on
|
|
394
|
+
* `isApproved(approvals)` before the journal entry is committed.
|
|
395
|
+
* Automatic transactions (earn from purchase, expiry sweeps) skip the
|
|
396
|
+
* chain entirely — single-tier flows leave this null.
|
|
397
|
+
*/
|
|
398
|
+
approvals?: ApprovalChain | null;
|
|
399
|
+
/** Snapshot of the matrix policy id that produced this chain (host-managed). */
|
|
400
|
+
approvalPolicyId?: string | null;
|
|
401
|
+
/** Version of the matrix policy at submit time — frozen for audit. */
|
|
402
|
+
approvalPolicyVersion?: number | null;
|
|
403
|
+
createdAt: Date;
|
|
404
|
+
}
|
|
405
|
+
//#endregion
|
|
406
|
+
//#region src/types/results.d.ts
|
|
407
|
+
interface BalanceSummary extends MemberBalance {
|
|
408
|
+
tier: string | null;
|
|
409
|
+
tierOverride: string | null;
|
|
410
|
+
}
|
|
411
|
+
interface EarningBreakdown {
|
|
412
|
+
ruleId: string;
|
|
413
|
+
ruleName: string;
|
|
414
|
+
points: number;
|
|
415
|
+
}
|
|
416
|
+
interface EarningResult {
|
|
417
|
+
totalPoints: number;
|
|
418
|
+
breakdown: EarningBreakdown[];
|
|
419
|
+
transactions: PointTransaction[];
|
|
420
|
+
}
|
|
421
|
+
interface TierEvaluation {
|
|
422
|
+
memberId: string;
|
|
423
|
+
previousTier: string | null;
|
|
424
|
+
newTier: string | null;
|
|
425
|
+
action: 'upgraded' | 'downgraded' | 'grace_period' | 'unchanged';
|
|
426
|
+
graceExpiresAt?: Date;
|
|
427
|
+
}
|
|
428
|
+
interface TierEvaluationBatch {
|
|
429
|
+
upgraded: number;
|
|
430
|
+
downgraded: number;
|
|
431
|
+
unchanged: number;
|
|
432
|
+
}
|
|
433
|
+
interface RedemptionValidation {
|
|
434
|
+
valid: boolean;
|
|
435
|
+
error?: string;
|
|
436
|
+
pointsToRedeem: number;
|
|
437
|
+
/**
|
|
438
|
+
* Discount in **integer minor units** (paisa for BDT) — always exactly
|
|
439
|
+
* `pointsToRedeem × pointValueMinor`.
|
|
440
|
+
*/
|
|
441
|
+
discountAmount: number;
|
|
442
|
+
maxAllowedPoints: number;
|
|
443
|
+
}
|
|
444
|
+
/** What the program owes, in points and in integer minor units at the current point value. */
|
|
445
|
+
interface LoyaltyLiabilitySnapshot {
|
|
446
|
+
spendablePoints: number;
|
|
447
|
+
/** Held by open reservations — out of the balance, not yet used. */
|
|
448
|
+
reservedPoints: number;
|
|
449
|
+
outstandingPoints: number;
|
|
450
|
+
pointValueMinor: number;
|
|
451
|
+
valueMinor: number;
|
|
452
|
+
}
|
|
453
|
+
interface ExpirationResult {
|
|
454
|
+
membersAffected: number;
|
|
455
|
+
pointsExpired: number;
|
|
456
|
+
transactionCount: number;
|
|
457
|
+
}
|
|
458
|
+
/**
|
|
459
|
+
* Outcome of `PointTransactionRepository.reverseEarned` for one member.
|
|
460
|
+
* `claimed` is what the refund entitles the program to take back; `collected` is
|
|
461
|
+
* what was actually debited. A non-zero `shortfall` means the member had already
|
|
462
|
+
* spent those points — reported, never silently written off.
|
|
463
|
+
*/
|
|
464
|
+
interface EarnReversalResult {
|
|
465
|
+
memberId: string | null;
|
|
466
|
+
earned: number;
|
|
467
|
+
/** Points that should stand reversed after this call (`floor(earned × fraction)`). */
|
|
468
|
+
target: number;
|
|
469
|
+
alreadyClaimed: number;
|
|
470
|
+
claimed: number;
|
|
471
|
+
collected: number;
|
|
472
|
+
shortfall: number;
|
|
473
|
+
transactionId: string | null;
|
|
474
|
+
}
|
|
475
|
+
interface PaginatedResult<T> {
|
|
476
|
+
docs: T[];
|
|
477
|
+
total: number;
|
|
478
|
+
page: number;
|
|
479
|
+
pages: number;
|
|
480
|
+
hasNext: boolean;
|
|
481
|
+
hasPrev: boolean;
|
|
482
|
+
}
|
|
483
|
+
interface ListQuery {
|
|
484
|
+
page?: number;
|
|
485
|
+
limit?: number;
|
|
486
|
+
sort?: Record<string, 1 | -1>;
|
|
487
|
+
}
|
|
488
|
+
//#endregion
|
|
489
|
+
//#region src/types/index.d.ts
|
|
490
|
+
/**
|
|
491
|
+
* Context passed to every loyalty operation.
|
|
492
|
+
*
|
|
493
|
+
* The tenant key is dynamic — if you configured `tenant: { field: 'companyId', contextKey: 'companyId' }`,
|
|
494
|
+
* pass `{ companyId: '...' }` in the context. The `[key: string]: unknown` index allows this.
|
|
495
|
+
*
|
|
496
|
+
* For the default config (`tenant: true` or `tenant: undefined`), pass `organizationId`.
|
|
497
|
+
* For `tenant: false`, the tenant key is ignored.
|
|
498
|
+
*/
|
|
499
|
+
/**
|
|
500
|
+
* Extends `@classytic/primitives`' {@link OperationContext} with loyalty-
|
|
501
|
+
* specific fields. `actorId` is narrowed to a required `string` since every
|
|
502
|
+
* loyalty mutation is attributed; other primitives fields (`correlationId`,
|
|
503
|
+
* `traceId`, `requestId`, `session`, `metadata`) flow through unchanged.
|
|
504
|
+
*/
|
|
505
|
+
interface LoyaltyContext extends OperationContext {
|
|
506
|
+
/** Required for loyalty: who is performing this operation. Narrowed to string. */
|
|
507
|
+
actorId: string;
|
|
508
|
+
/** Narrowed from primitives' `IdLike` to string. */
|
|
509
|
+
organizationId?: string;
|
|
510
|
+
/** Override program ID for multi-program setups. */
|
|
511
|
+
programId?: string;
|
|
512
|
+
/**
|
|
513
|
+
* Cooperative cancellation for long-running cron loops (point expiry,
|
|
514
|
+
* reservation cleanup, tier evaluation, override sweeps, campaign award).
|
|
515
|
+
* Checked at each loop boundary via `throwIfAborted` — aborting between
|
|
516
|
+
* iterations never rolls back an already-committed transaction, and each
|
|
517
|
+
* per-item write is its own unit of work. Threaded to mongokit round-trips
|
|
518
|
+
* by hosts that pass it; mongokit (3.16+) honours it at every op boundary.
|
|
519
|
+
*/
|
|
520
|
+
signal?: AbortSignal | undefined;
|
|
521
|
+
/** Dynamic tenant key support — e.g. `{ companyId: '...' }` — when a host
|
|
522
|
+
* configured a custom `contextKey`. */
|
|
523
|
+
[key: string]: unknown;
|
|
524
|
+
}
|
|
525
|
+
interface EnrollInput {
|
|
526
|
+
externalId: string;
|
|
527
|
+
externalType: string;
|
|
528
|
+
programId?: string;
|
|
529
|
+
tier?: string;
|
|
530
|
+
cardId?: string;
|
|
531
|
+
metadata?: Record<string, unknown>;
|
|
532
|
+
}
|
|
533
|
+
interface EarnPointsInput {
|
|
534
|
+
memberId: string;
|
|
535
|
+
points: number;
|
|
536
|
+
description: string;
|
|
537
|
+
referenceType?: string;
|
|
538
|
+
referenceId?: string;
|
|
539
|
+
earningRuleId?: string;
|
|
540
|
+
expiresAt?: Date;
|
|
541
|
+
idempotencyKey?: string;
|
|
542
|
+
metadata?: Record<string, unknown>;
|
|
543
|
+
}
|
|
544
|
+
interface AdjustPointsInput {
|
|
545
|
+
memberId: string;
|
|
546
|
+
points: number;
|
|
547
|
+
description: string;
|
|
548
|
+
reason: string;
|
|
549
|
+
referenceType?: string;
|
|
550
|
+
referenceId?: string;
|
|
551
|
+
idempotencyKey?: string;
|
|
552
|
+
metadata?: Record<string, unknown>;
|
|
553
|
+
}
|
|
554
|
+
interface CreateEarningRuleInput {
|
|
555
|
+
name: string;
|
|
556
|
+
type: 'order' | 'action' | 'category' | 'tier_bonus';
|
|
557
|
+
priority?: number;
|
|
558
|
+
description?: string;
|
|
559
|
+
conditions?: EarningConditions;
|
|
560
|
+
reward: EarningReward;
|
|
561
|
+
startsAt?: Date | string;
|
|
562
|
+
endsAt?: Date | string;
|
|
563
|
+
/**
|
|
564
|
+
* Loyalty program this rule belongs to.
|
|
565
|
+
*
|
|
566
|
+
* Optional for repository-created rules: `EarningRuleRepository.create(...)`
|
|
567
|
+
* and `createMany(...)` fill this from `LoyaltyShape.program.id`, matching
|
|
568
|
+
* the default used by `MemberRepository.enroll(...)`. Pass explicitly only
|
|
569
|
+
* when creating a rule for a different program.
|
|
570
|
+
*/
|
|
571
|
+
programId?: string;
|
|
572
|
+
metadata?: Record<string, unknown>;
|
|
573
|
+
}
|
|
574
|
+
interface UpdateEarningRuleInput {
|
|
575
|
+
name?: string;
|
|
576
|
+
priority?: number;
|
|
577
|
+
description?: string;
|
|
578
|
+
conditions?: EarningConditions;
|
|
579
|
+
reward?: EarningReward;
|
|
580
|
+
startsAt?: Date | string;
|
|
581
|
+
endsAt?: Date | string;
|
|
582
|
+
metadata?: Record<string, unknown>;
|
|
583
|
+
}
|
|
584
|
+
interface CreateTierInput {
|
|
585
|
+
name: string;
|
|
586
|
+
rank: number;
|
|
587
|
+
qualificationCriteria?: TierQualification;
|
|
588
|
+
benefits?: TierBenefits;
|
|
589
|
+
downgrade?: TierDowngrade;
|
|
590
|
+
color?: string;
|
|
591
|
+
programId?: string;
|
|
592
|
+
metadata?: Record<string, unknown>;
|
|
593
|
+
}
|
|
594
|
+
interface UpdateTierInput {
|
|
595
|
+
name?: string;
|
|
596
|
+
rank?: number;
|
|
597
|
+
qualificationCriteria?: TierQualification;
|
|
598
|
+
benefits?: TierBenefits;
|
|
599
|
+
downgrade?: TierDowngrade;
|
|
600
|
+
color?: string;
|
|
601
|
+
metadata?: Record<string, unknown>;
|
|
602
|
+
}
|
|
603
|
+
//#endregion
|
|
604
|
+
export { RepositoryPlugins as A, IndexDefinition as C, LoyaltyShape as D, LoyaltyRuntime as E, StampVoucherRequest as F, TierDefinition as I, EarningRule as L, ResolvedTenant as M, TenantConfig$1 as N, ModelName as O, StampRewardIssuer as P, MemberBalance as S, LoyaltyModelName as T, RedemptionValidation as _, EnrollInput as a, PointTransaction as b, UpdateTierInput as c, EarningBreakdown as d, EarningResult as f, PaginatedResult as g, LoyaltyLiabilitySnapshot as h, EarnPointsInput as i, ResolvedConfig as j, PluginType as k, BalanceSummary as l, ListQuery as m, CreateEarningRuleInput as n, LoyaltyContext as o, ExpirationResult as p, CreateTierInput as r, UpdateEarningRuleInput as s, AdjustPointsInput as t, EarnReversalResult as u, TierEvaluation as v, LiveProgramSettings as w, Member as x, TierEvaluationBatch as y };
|