@classytic/loyalty 0.2.3 → 0.4.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 +114 -0
- package/README.md +69 -35
- package/dist/{config-96Sl6oNJ.d.mts → config-Dtun1duw.d.mts} +23 -0
- package/dist/events/index.d.mts +1 -1
- package/dist/events/index.mjs +2 -2
- package/dist/{events-BtZOdiPw.mjs → events-DN5_p6T0.mjs} +1 -1
- package/dist/{index-DArT4zkB.d.mts → index-D_5Ojc2c.d.mts} +1 -1
- package/dist/{index-Cl-WGBFJ.d.mts → index-Dngaj21_.d.mts} +19 -5
- package/dist/{index-DWarXsmw.d.mts → index-YSsi4e_G.d.mts} +15 -0
- package/dist/index.d.mts +154 -6
- package/dist/index.mjs +29 -29
- package/dist/{loyalty-events-Cyt9TwX6.mjs → loyalty-events-C38JA3cU.mjs} +4 -2
- package/dist/models/index.d.mts +497 -497
- package/dist/models/index.mjs +1 -1
- package/dist/{models-0_OQIqBy.mjs → models-Ddh62lgO.mjs} +4 -1
- package/dist/repositories/index.d.mts +1 -1
- package/dist/repositories/index.mjs +1 -1
- package/dist/{repositories-BMwidCp-.mjs → repositories-DEwCbLCh.mjs} +325 -160
- package/dist/types/index.d.mts +2 -2
- package/package.json +7 -7
|
@@ -1,7 +1,20 @@
|
|
|
1
|
-
import { n as createEvent, t as LoyaltyEvents } from "./loyalty-events-
|
|
1
|
+
import { n as createEvent, t as LoyaltyEvents } from "./loyalty-events-C38JA3cU.mjs";
|
|
2
2
|
import { Repository, multiTenantPlugin } from "@classytic/mongokit";
|
|
3
|
+
import { localTimeParts } from "@classytic/primitives/timezone";
|
|
4
|
+
import { addDays, isoWeekday } from "@classytic/primitives/calendar";
|
|
3
5
|
import { randomBytes } from "node:crypto";
|
|
6
|
+
import { throwIfAborted } from "@classytic/repo-core/repository";
|
|
4
7
|
//#region src/domain/errors/base.ts
|
|
8
|
+
/**
|
|
9
|
+
* Base for every loyalty throwable. Implements `HttpError` from
|
|
10
|
+
* `@classytic/repo-core/errors` (the org-canonical throwable contract) so arc
|
|
11
|
+
* catches and serializes them to `ErrorContract` via `toErrorContract()` with
|
|
12
|
+
* the right HTTP `status` and a stable, machine-readable `code`.
|
|
13
|
+
*
|
|
14
|
+
* Codes are hierarchical + lowercase per PACKAGE_RULES ("Errors and wire
|
|
15
|
+
* envelopes"): `loyalty.<entity>.<problem>`. The `status` field (not the old
|
|
16
|
+
* `httpStatus`) is what `HttpError` / arc read — a clean break, no alias.
|
|
17
|
+
*/
|
|
5
18
|
var LoyaltyError = class extends Error {
|
|
6
19
|
constructor(message) {
|
|
7
20
|
super(message);
|
|
@@ -11,107 +24,147 @@ var LoyaltyError = class extends Error {
|
|
|
11
24
|
return {
|
|
12
25
|
code: this.code,
|
|
13
26
|
message: this.message,
|
|
14
|
-
name: this.name
|
|
27
|
+
name: this.name,
|
|
28
|
+
status: this.status
|
|
15
29
|
};
|
|
16
30
|
}
|
|
17
31
|
};
|
|
18
32
|
//#endregion
|
|
19
33
|
//#region src/domain/errors/domain-errors.ts
|
|
20
34
|
var InsufficientPointsError = class extends LoyaltyError {
|
|
21
|
-
code = "
|
|
22
|
-
|
|
35
|
+
code = "loyalty.points.insufficient";
|
|
36
|
+
status = 400;
|
|
23
37
|
constructor(available, requested) {
|
|
24
38
|
super(`Insufficient points. Available: ${available}, requested: ${requested}`);
|
|
25
39
|
}
|
|
26
40
|
};
|
|
27
41
|
var MemberNotFoundError = class extends LoyaltyError {
|
|
28
|
-
code = "
|
|
29
|
-
|
|
42
|
+
code = "loyalty.member.not_found";
|
|
43
|
+
status = 404;
|
|
30
44
|
constructor(id) {
|
|
31
45
|
super(`Member not found: ${id}`);
|
|
32
46
|
}
|
|
33
47
|
};
|
|
34
48
|
var MemberAlreadyEnrolledError = class extends LoyaltyError {
|
|
35
|
-
code = "
|
|
36
|
-
|
|
49
|
+
code = "loyalty.member.already_enrolled";
|
|
50
|
+
status = 409;
|
|
37
51
|
constructor(externalId) {
|
|
38
52
|
super(`Member already enrolled: ${externalId}`);
|
|
39
53
|
}
|
|
40
54
|
};
|
|
41
55
|
var MemberInactiveError = class extends LoyaltyError {
|
|
42
|
-
code = "
|
|
43
|
-
|
|
56
|
+
code = "loyalty.member.inactive";
|
|
57
|
+
status = 422;
|
|
44
58
|
constructor(id) {
|
|
45
59
|
super(`Member is not active: ${id}`);
|
|
46
60
|
}
|
|
47
61
|
};
|
|
62
|
+
var RuleNotFoundError = class extends LoyaltyError {
|
|
63
|
+
code = "loyalty.earning_rule.not_found";
|
|
64
|
+
status = 404;
|
|
65
|
+
constructor(id) {
|
|
66
|
+
super(`Earning rule not found: ${id}`);
|
|
67
|
+
}
|
|
68
|
+
};
|
|
48
69
|
var TierNotFoundError = class extends LoyaltyError {
|
|
49
|
-
code = "
|
|
50
|
-
|
|
70
|
+
code = "loyalty.tier.not_found";
|
|
71
|
+
status = 404;
|
|
51
72
|
constructor(id) {
|
|
52
73
|
super(`Tier not found: ${id}`);
|
|
53
74
|
}
|
|
54
75
|
};
|
|
55
76
|
var RedemptionNotFoundError = class extends LoyaltyError {
|
|
56
|
-
code = "
|
|
57
|
-
|
|
77
|
+
code = "loyalty.redemption.not_found";
|
|
78
|
+
status = 404;
|
|
58
79
|
constructor(id) {
|
|
59
80
|
super(`Redemption not found: ${id}`);
|
|
60
81
|
}
|
|
61
82
|
};
|
|
62
83
|
var RedemptionExpiredError = class extends LoyaltyError {
|
|
63
|
-
code = "
|
|
64
|
-
|
|
84
|
+
code = "loyalty.redemption.expired";
|
|
85
|
+
status = 410;
|
|
65
86
|
constructor(id) {
|
|
66
87
|
super(`Redemption expired: ${id}`);
|
|
67
88
|
}
|
|
68
89
|
};
|
|
69
90
|
var ReferralNotFoundError = class extends LoyaltyError {
|
|
70
|
-
code = "
|
|
71
|
-
|
|
91
|
+
code = "loyalty.referral.not_found";
|
|
92
|
+
status = 404;
|
|
72
93
|
constructor(id) {
|
|
73
94
|
super(`Referral not found: ${id}`);
|
|
74
95
|
}
|
|
75
96
|
};
|
|
76
97
|
var DuplicateReferralError = class extends LoyaltyError {
|
|
77
|
-
code = "
|
|
78
|
-
|
|
98
|
+
code = "loyalty.referral.duplicate";
|
|
99
|
+
status = 409;
|
|
79
100
|
constructor(refereeId) {
|
|
80
101
|
super(`Referee already has a referral: ${refereeId}`);
|
|
81
102
|
}
|
|
82
103
|
};
|
|
83
104
|
var ReferralLimitExceededError = class extends LoyaltyError {
|
|
84
|
-
code = "
|
|
85
|
-
|
|
105
|
+
code = "loyalty.referral.limit_exceeded";
|
|
106
|
+
status = 429;
|
|
86
107
|
constructor(limit, periodDays) {
|
|
87
108
|
super(`Referral limit of ${limit} per ${periodDays} days exceeded`);
|
|
88
109
|
}
|
|
89
110
|
};
|
|
90
111
|
var SelfReferralError = class extends LoyaltyError {
|
|
91
|
-
code = "
|
|
92
|
-
|
|
112
|
+
code = "loyalty.referral.self";
|
|
113
|
+
status = 422;
|
|
93
114
|
constructor() {
|
|
94
115
|
super("Cannot refer yourself");
|
|
95
116
|
}
|
|
96
117
|
};
|
|
97
118
|
var CircularReferralError = class extends LoyaltyError {
|
|
98
|
-
code = "
|
|
99
|
-
|
|
119
|
+
code = "loyalty.referral.circular";
|
|
120
|
+
status = 422;
|
|
100
121
|
constructor() {
|
|
101
122
|
super("Circular referral detected");
|
|
102
123
|
}
|
|
103
124
|
};
|
|
104
125
|
var ValidationError = class extends LoyaltyError {
|
|
105
|
-
code = "
|
|
106
|
-
|
|
126
|
+
code = "loyalty.validation.invalid";
|
|
127
|
+
status = 400;
|
|
107
128
|
};
|
|
108
129
|
var TenantIsolationError = class extends LoyaltyError {
|
|
109
|
-
code = "
|
|
110
|
-
|
|
130
|
+
code = "loyalty.tenant.isolation";
|
|
131
|
+
status = 403;
|
|
111
132
|
constructor() {
|
|
112
133
|
super("Tenant context is required but not provided");
|
|
113
134
|
}
|
|
114
135
|
};
|
|
136
|
+
/**
|
|
137
|
+
* A host passed `ctx.session` (it owns the outer transaction) but did NOT
|
|
138
|
+
* attach a `[PENDING_EVENTS]` queue. Publishing to in-process subscribers
|
|
139
|
+
* BEFORE the host's commit would leak ghost events on rollback, and the
|
|
140
|
+
* outbox save would not be coordinated with the host's unit of work.
|
|
141
|
+
*
|
|
142
|
+
* Two safe patterns: (1) let loyalty own the transaction — call the verb
|
|
143
|
+
* WITHOUT `ctx.session`; or (2) attach a fresh `[PENDING_EVENTS]` array via
|
|
144
|
+
* `{ ...ctx, [PENDING_EVENTS]: queue }` and call `flushPendingLoyaltyEvents`
|
|
145
|
+
* after your transaction commits. Both are exported from the package root.
|
|
146
|
+
*/
|
|
147
|
+
var UnmanagedSessionError = class extends LoyaltyError {
|
|
148
|
+
code = "loyalty.transaction.unmanaged_session";
|
|
149
|
+
status = 400;
|
|
150
|
+
constructor() {
|
|
151
|
+
super("[loyalty] ctx.session was provided without a PENDING_EVENTS queue. When you own the outer transaction you must also coordinate event publishing — otherwise loyalty would publish to in-process subscribers (and save to the outbox) before your transaction commits, leaking ghost events on rollback. Either call the verb without ctx.session (loyalty owns the unit of work), or attach `{ ...ctx, [PENDING_EVENTS]: queue }` and call `flushPendingLoyaltyEvents(transport, queue)` after your commit. Both `PENDING_EVENTS` and `flushPendingLoyaltyEvents` are exported from `@classytic/loyalty`.");
|
|
152
|
+
}
|
|
153
|
+
};
|
|
154
|
+
/**
|
|
155
|
+
* The backend doesn't declare the runtime capabilities loyalty's
|
|
156
|
+
* transactional verbs require (earn / adjust / reserve / confirm / release /
|
|
157
|
+
* reverse all run inside `withTransaction`). MongoDB must run as a replica set
|
|
158
|
+
* or sharded cluster. Thrown at engine creation — fail loud at boot, not with
|
|
159
|
+
* a cryptic driver error on the first transactional call.
|
|
160
|
+
*/
|
|
161
|
+
var LoyaltyCapabilityError = class extends LoyaltyError {
|
|
162
|
+
code = "loyalty.engine.missing_capabilities";
|
|
163
|
+
status = 500;
|
|
164
|
+
constructor(missing) {
|
|
165
|
+
super(`[loyalty] backend missing required capabilities: ${missing.join(", ")}. Point earn / redemption / reversal paths are transactional — MongoDB must run as a replica set or sharded cluster.`);
|
|
166
|
+
}
|
|
167
|
+
};
|
|
115
168
|
//#endregion
|
|
116
169
|
//#region src/utils/tenant-guard.ts
|
|
117
170
|
/**
|
|
@@ -282,6 +335,7 @@ var EarningRuleRepository = class extends Repository {
|
|
|
282
335
|
const transactions = [];
|
|
283
336
|
let totalPoints = 0;
|
|
284
337
|
for (const rule of actionRules) {
|
|
338
|
+
if (!this.matchesConditions(rule, member)) continue;
|
|
285
339
|
const points = rule.reward.fixedPoints ?? 0;
|
|
286
340
|
if (points <= 0) continue;
|
|
287
341
|
const tx = await this.txRepo.earnPoints({
|
|
@@ -289,8 +343,11 @@ var EarningRuleRepository = class extends Repository {
|
|
|
289
343
|
points,
|
|
290
344
|
description: `Action: ${input.action} (${rule.name})`,
|
|
291
345
|
referenceType: "action",
|
|
292
|
-
|
|
293
|
-
|
|
346
|
+
earningRuleId: rule._id,
|
|
347
|
+
...input.referenceId !== void 0 ? {
|
|
348
|
+
referenceId: input.referenceId,
|
|
349
|
+
idempotencyKey: `${input.referenceId}:${rule._id}`
|
|
350
|
+
} : {}
|
|
294
351
|
}, ctx);
|
|
295
352
|
totalPoints += points;
|
|
296
353
|
breakdown.push({
|
|
@@ -306,10 +363,14 @@ var EarningRuleRepository = class extends Repository {
|
|
|
306
363
|
transactions
|
|
307
364
|
};
|
|
308
365
|
}
|
|
366
|
+
/**
|
|
367
|
+
* `orderTotal` is optional — action-type evaluations have no order, so
|
|
368
|
+
* `minOrderAmount` only applies when a total is supplied (order paths).
|
|
369
|
+
*/
|
|
309
370
|
matchesConditions(rule, member, orderTotal) {
|
|
310
371
|
const { conditions } = rule;
|
|
311
372
|
const now = /* @__PURE__ */ new Date();
|
|
312
|
-
if (conditions.minOrderAmount && orderTotal < conditions.minOrderAmount) return false;
|
|
373
|
+
if (conditions.minOrderAmount && orderTotal !== void 0 && orderTotal < conditions.minOrderAmount) return false;
|
|
313
374
|
if (conditions.tiers && conditions.tiers.length > 0) {
|
|
314
375
|
const memberTier = member.tierOverride ?? member.tier;
|
|
315
376
|
if (!memberTier || !conditions.tiers.includes(memberTier)) return false;
|
|
@@ -319,10 +380,20 @@ var EarningRuleRepository = class extends Repository {
|
|
|
319
380
|
if (conditions.dateRange.end && now > conditions.dateRange.end) return false;
|
|
320
381
|
}
|
|
321
382
|
if (conditions.dayOfWeek && conditions.dayOfWeek.length > 0) {
|
|
322
|
-
if (!conditions.dayOfWeek.includes(
|
|
383
|
+
if (!conditions.dayOfWeek.includes(this.weekday(now))) return false;
|
|
323
384
|
}
|
|
324
385
|
return true;
|
|
325
386
|
}
|
|
387
|
+
/**
|
|
388
|
+
* The wall-clock weekday of `instant` in JS `getDay()` convention
|
|
389
|
+
* (0=Sun … 6=Sat — what `EarningConditions.dayOfWeek` is authored in),
|
|
390
|
+
* evaluated in the engine's configured `timezone` (DST-exact) or UTC when
|
|
391
|
+
* unset. NEVER server-local `Date.getDay()`, which shifts with the deploy
|
|
392
|
+
* machine's `TZ` env (PACKAGE_RULES P12).
|
|
393
|
+
*/
|
|
394
|
+
weekday(instant) {
|
|
395
|
+
return (this.config.timezone ? localTimeParts(instant, this.config.timezone).isoWeekday : isoWeekday(instant)) % 7;
|
|
396
|
+
}
|
|
326
397
|
calculateOrderPoints(rule, orderTotal) {
|
|
327
398
|
const { reward } = rule;
|
|
328
399
|
let points = 0;
|
|
@@ -349,7 +420,16 @@ var EarningRuleRepository = class extends Repository {
|
|
|
349
420
|
};
|
|
350
421
|
//#endregion
|
|
351
422
|
//#region src/events/dispatch.ts
|
|
423
|
+
/**
|
|
424
|
+
* Internal key under which the post-commit event-publish queue is threaded
|
|
425
|
+
* through the context. When set, `dispatchLoyaltyEvent` APPENDS to this queue
|
|
426
|
+
* instead of publishing immediately — the verb that owns the transaction
|
|
427
|
+
* flushes it after commit. A `Symbol.for` so it never collides with
|
|
428
|
+
* host-supplied context fields and never appears in the public type.
|
|
429
|
+
*/
|
|
430
|
+
const PENDING_EVENTS = Symbol.for("@classytic/loyalty:pendingEvents");
|
|
352
431
|
async function dispatchLoyaltyEvent(deps, event, ctx) {
|
|
432
|
+
const pending = ctx[PENDING_EVENTS];
|
|
353
433
|
if (deps.outbox) try {
|
|
354
434
|
const saveOptions = {};
|
|
355
435
|
if (ctx.session !== void 0) saveOptions.session = ctx.session;
|
|
@@ -358,12 +438,64 @@ async function dispatchLoyaltyEvent(deps, event, ctx) {
|
|
|
358
438
|
deps.logger?.error(`[loyalty] outbox.save failed for ${event.type}:`, err);
|
|
359
439
|
throw err;
|
|
360
440
|
}
|
|
361
|
-
if (
|
|
441
|
+
else if (ctx.session && !pending) throw new UnmanagedSessionError();
|
|
442
|
+
if (pending) {
|
|
443
|
+
pending.push(event);
|
|
444
|
+
return;
|
|
445
|
+
}
|
|
446
|
+
if (deps.events && !ctx.session) try {
|
|
362
447
|
await deps.events.publish(event);
|
|
363
448
|
} catch (err) {
|
|
364
449
|
deps.logger?.error(`[loyalty] events.publish failed for ${event.type}:`, err);
|
|
365
450
|
}
|
|
366
451
|
}
|
|
452
|
+
/**
|
|
453
|
+
* Runs a multi-write domain verb inside a unit-of-work transaction with the
|
|
454
|
+
* P8 post-commit-publish discipline (PACKAGE_RULES §P8 phased variant):
|
|
455
|
+
*
|
|
456
|
+
* 1. A fresh `[PENDING_EVENTS]` queue is attached to the inner ctx along
|
|
457
|
+
* with the transaction `session`. Dispatches inside `body` save the
|
|
458
|
+
* outbox row in-session (atomic with the business write) and queue the
|
|
459
|
+
* transport publish instead of firing it.
|
|
460
|
+
* 2. The queue is reset at the START of each attempt — a transient-error
|
|
461
|
+
* retry of `body` can't leak ghost events.
|
|
462
|
+
* 3. After the transaction COMMITS, the queue is flushed to the transport.
|
|
463
|
+
*
|
|
464
|
+
* When the host already owns the outer transaction (`ctx.session` set), it
|
|
465
|
+
* MUST have attached a `[PENDING_EVENTS]` queue — `body` runs directly against
|
|
466
|
+
* the host's session+queue and the host flushes after its own commit. A bare
|
|
467
|
+
* `ctx.session` with no queue throws `UnmanagedSessionError` (raised inside
|
|
468
|
+
* `dispatchLoyaltyEvent` at the first emit).
|
|
469
|
+
*/
|
|
470
|
+
async function runLoyaltyUnit(unitOfWork, deps, ctx, body) {
|
|
471
|
+
const internalCtx = ctx;
|
|
472
|
+
if (ctx.session) return body(internalCtx);
|
|
473
|
+
const pending = [];
|
|
474
|
+
const result = await unitOfWork.withTransaction(async (session) => {
|
|
475
|
+
pending.length = 0;
|
|
476
|
+
return body({
|
|
477
|
+
...internalCtx,
|
|
478
|
+
session,
|
|
479
|
+
[PENDING_EVENTS]: pending
|
|
480
|
+
});
|
|
481
|
+
});
|
|
482
|
+
await flushPendingLoyaltyEvents(deps.events, pending, deps.logger);
|
|
483
|
+
return result;
|
|
484
|
+
}
|
|
485
|
+
/**
|
|
486
|
+
* Drains a queue of deferred events (the one attached under `[PENDING_EVENTS]`)
|
|
487
|
+
* through the transport, swallowing per-event errors. Called by transactional
|
|
488
|
+
* verbs AFTER `withTransaction` commits, and exported so hosts that own the
|
|
489
|
+
* outer transaction can flush after their own commit.
|
|
490
|
+
*/
|
|
491
|
+
async function flushPendingLoyaltyEvents(events, queue, logger) {
|
|
492
|
+
if (!events || queue.length === 0) return;
|
|
493
|
+
for (const event of queue) try {
|
|
494
|
+
await events.publish(event);
|
|
495
|
+
} catch (err) {
|
|
496
|
+
logger?.error(`[loyalty] events.publish failed for ${event.type}:`, err);
|
|
497
|
+
}
|
|
498
|
+
}
|
|
367
499
|
//#endregion
|
|
368
500
|
//#region src/utils/code-generator.ts
|
|
369
501
|
const CHARSET = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789";
|
|
@@ -586,6 +718,12 @@ var PointTransactionRepository = class extends Repository {
|
|
|
586
718
|
outbox: this.outbox
|
|
587
719
|
}, event, ctx);
|
|
588
720
|
}
|
|
721
|
+
get _dispatchDeps() {
|
|
722
|
+
return {
|
|
723
|
+
events: this.events,
|
|
724
|
+
outbox: this.outbox
|
|
725
|
+
};
|
|
726
|
+
}
|
|
589
727
|
/**
|
|
590
728
|
* Auto-inject tenant id from options into doc payload. Matches
|
|
591
729
|
* `@classytic/order` / `@classytic/promo` — see MemberRepository.
|
|
@@ -613,7 +751,9 @@ var PointTransactionRepository = class extends Repository {
|
|
|
613
751
|
}
|
|
614
752
|
const member = await this.memberRepo.requireActiveMember(input.memberId, tc);
|
|
615
753
|
try {
|
|
616
|
-
|
|
754
|
+
let payload;
|
|
755
|
+
const tx = await runLoyaltyUnit(this.unitOfWork, this._dispatchDeps, ctx, async (txCtx) => {
|
|
756
|
+
const session = txCtx.session;
|
|
617
757
|
if (input.idempotencyKey) {
|
|
618
758
|
const dup = await this.getByQuery({ idempotencyKey: input.idempotencyKey }, {
|
|
619
759
|
throwOnNotFound: false,
|
|
@@ -643,26 +783,27 @@ var PointTransactionRepository = class extends Repository {
|
|
|
643
783
|
...input.metadata ? { metadata: input.metadata } : {}
|
|
644
784
|
};
|
|
645
785
|
if (input.idempotencyKey) data.idempotencyKey = input.idempotencyKey;
|
|
646
|
-
|
|
786
|
+
const created = await this.create(data, {
|
|
647
787
|
session,
|
|
648
788
|
...tc
|
|
649
789
|
});
|
|
790
|
+
payload = {
|
|
791
|
+
memberId: member._id,
|
|
792
|
+
externalId: member.externalId,
|
|
793
|
+
externalType: member.externalType,
|
|
794
|
+
points: input.points,
|
|
795
|
+
balanceAfter: created.balanceAfter,
|
|
796
|
+
transactionId: created._id,
|
|
797
|
+
referenceType: input.referenceType,
|
|
798
|
+
referenceId: input.referenceId
|
|
799
|
+
};
|
|
800
|
+
await this._dispatch(createEvent(LoyaltyEvents.POINTS_EARNED, payload, txCtx, {
|
|
801
|
+
resource: "pointTransaction",
|
|
802
|
+
resourceId: created._id
|
|
803
|
+
}), txCtx);
|
|
804
|
+
return created;
|
|
650
805
|
});
|
|
651
|
-
|
|
652
|
-
memberId: member._id,
|
|
653
|
-
externalId: member.externalId,
|
|
654
|
-
externalType: member.externalType,
|
|
655
|
-
points: input.points,
|
|
656
|
-
balanceAfter: tx.balanceAfter,
|
|
657
|
-
transactionId: tx._id,
|
|
658
|
-
referenceType: input.referenceType,
|
|
659
|
-
referenceId: input.referenceId
|
|
660
|
-
};
|
|
661
|
-
await this._dispatch(createEvent(LoyaltyEvents.POINTS_EARNED, payload, ctx, {
|
|
662
|
-
resource: "pointTransaction",
|
|
663
|
-
resourceId: tx._id
|
|
664
|
-
}), ctx);
|
|
665
|
-
this.notifications?.notify(LoyaltyEvents.POINTS_EARNED, { ...payload });
|
|
806
|
+
if (payload) this.notifications?.notify(LoyaltyEvents.POINTS_EARNED, { ...payload });
|
|
666
807
|
return tx;
|
|
667
808
|
} catch (err) {
|
|
668
809
|
if (input.idempotencyKey && (isDuplicateKey(err) || isWriteConflict(err))) {
|
|
@@ -696,6 +837,7 @@ var PointTransactionRepository = class extends Repository {
|
|
|
696
837
|
let totalPoints = 0;
|
|
697
838
|
const errors = [];
|
|
698
839
|
for (const row of members) {
|
|
840
|
+
throwIfAborted(ctx.signal);
|
|
699
841
|
try {
|
|
700
842
|
validatePositivePoints(row.points);
|
|
701
843
|
} catch (err) {
|
|
@@ -770,7 +912,9 @@ var PointTransactionRepository = class extends Repository {
|
|
|
770
912
|
}
|
|
771
913
|
const member = await this.memberRepo.requireActiveMember(input.memberId, tc);
|
|
772
914
|
if (input.points < 0 && member.balance.current < Math.abs(input.points)) throw new InsufficientPointsError(member.balance.current, Math.abs(input.points));
|
|
773
|
-
|
|
915
|
+
let payload;
|
|
916
|
+
const tx = await runLoyaltyUnit(this.unitOfWork, this._dispatchDeps, ctx, async (txCtx) => {
|
|
917
|
+
const session = txCtx.session;
|
|
774
918
|
if (input.idempotencyKey) {
|
|
775
919
|
const dup = await this.getByQuery({ idempotencyKey: input.idempotencyKey }, {
|
|
776
920
|
throwOnNotFound: false,
|
|
@@ -801,24 +945,25 @@ var PointTransactionRepository = class extends Repository {
|
|
|
801
945
|
actorId: ctx.actorId
|
|
802
946
|
};
|
|
803
947
|
if (input.idempotencyKey) data.idempotencyKey = input.idempotencyKey;
|
|
804
|
-
|
|
948
|
+
const created = await this.create(data, {
|
|
805
949
|
session,
|
|
806
950
|
...tc
|
|
807
951
|
});
|
|
952
|
+
payload = {
|
|
953
|
+
memberId: member._id,
|
|
954
|
+
externalId: member.externalId,
|
|
955
|
+
points: input.points,
|
|
956
|
+
balanceAfter: created.balanceAfter,
|
|
957
|
+
transactionId: created._id,
|
|
958
|
+
description: input.description
|
|
959
|
+
};
|
|
960
|
+
await this._dispatch(createEvent(LoyaltyEvents.POINTS_ADJUSTED, payload, txCtx, {
|
|
961
|
+
resource: "pointTransaction",
|
|
962
|
+
resourceId: created._id
|
|
963
|
+
}), txCtx);
|
|
964
|
+
return created;
|
|
808
965
|
});
|
|
809
|
-
|
|
810
|
-
memberId: member._id,
|
|
811
|
-
externalId: member.externalId,
|
|
812
|
-
points: input.points,
|
|
813
|
-
balanceAfter: tx.balanceAfter,
|
|
814
|
-
transactionId: tx._id,
|
|
815
|
-
description: input.description
|
|
816
|
-
};
|
|
817
|
-
await this._dispatch(createEvent(LoyaltyEvents.POINTS_ADJUSTED, payload, ctx, {
|
|
818
|
-
resource: "pointTransaction",
|
|
819
|
-
resourceId: tx._id
|
|
820
|
-
}), ctx);
|
|
821
|
-
this.notifications?.notify(LoyaltyEvents.POINTS_ADJUSTED, { ...payload });
|
|
966
|
+
if (payload) this.notifications?.notify(LoyaltyEvents.POINTS_ADJUSTED, { ...payload });
|
|
822
967
|
return tx;
|
|
823
968
|
}
|
|
824
969
|
async processExpirations(ctx) {
|
|
@@ -838,6 +983,7 @@ var PointTransactionRepository = class extends Repository {
|
|
|
838
983
|
let transactionCount = 0;
|
|
839
984
|
const affectedMemberIds = /* @__PURE__ */ new Set();
|
|
840
985
|
for (const tx of expiredTxs) {
|
|
986
|
+
throwIfAborted(ctx.signal);
|
|
841
987
|
await this.unitOfWork.withTransaction(async (session) => {
|
|
842
988
|
await this.create({
|
|
843
989
|
memberId: tx.memberId,
|
|
@@ -936,6 +1082,12 @@ var RedemptionRepository = class extends Repository {
|
|
|
936
1082
|
outbox: this.outbox
|
|
937
1083
|
}, event, ctx);
|
|
938
1084
|
}
|
|
1085
|
+
get _dispatchDeps() {
|
|
1086
|
+
return {
|
|
1087
|
+
events: this.events,
|
|
1088
|
+
outbox: this.outbox
|
|
1089
|
+
};
|
|
1090
|
+
}
|
|
939
1091
|
/**
|
|
940
1092
|
* Atomic compare-and-set status transition.
|
|
941
1093
|
* Returns null if the current status doesn't match `expectedStatus` (CAS mismatch).
|
|
@@ -1005,7 +1157,9 @@ var RedemptionRepository = class extends Repository {
|
|
|
1005
1157
|
if (!validation.valid) throw new ValidationError(validation.error ?? "Redemption validation failed");
|
|
1006
1158
|
const member = await this.memberRepo.requireActiveMember(input.memberId, tc);
|
|
1007
1159
|
const expiresAt = input.expiresAt ?? new Date(Date.now() + this.config.redemption.reservationTtlMinutes * 60 * 1e3);
|
|
1008
|
-
|
|
1160
|
+
let payload;
|
|
1161
|
+
const redemption = await runLoyaltyUnit(this.unitOfWork, this._dispatchDeps, ctx, async (txCtx) => {
|
|
1162
|
+
const session = txCtx.session;
|
|
1009
1163
|
const updatedMember = await this.memberRepo.updateBalance(input.memberId, { current: -validation.pointsToRedeem }, {
|
|
1010
1164
|
session,
|
|
1011
1165
|
...tc
|
|
@@ -1023,25 +1177,26 @@ var RedemptionRepository = class extends Repository {
|
|
|
1023
1177
|
ownerId: input.ownerId,
|
|
1024
1178
|
expiresAt
|
|
1025
1179
|
};
|
|
1026
|
-
|
|
1180
|
+
const created = await this.create(data, {
|
|
1027
1181
|
session,
|
|
1028
1182
|
...tc
|
|
1029
1183
|
});
|
|
1184
|
+
payload = {
|
|
1185
|
+
memberId: member._id,
|
|
1186
|
+
externalId: member.externalId,
|
|
1187
|
+
redemptionId: created._id,
|
|
1188
|
+
points: validation.pointsToRedeem,
|
|
1189
|
+
discountAmount: validation.discountAmount,
|
|
1190
|
+
ownerType: input.ownerType,
|
|
1191
|
+
ownerId: input.ownerId
|
|
1192
|
+
};
|
|
1193
|
+
await this._dispatch(createEvent(LoyaltyEvents.REDEMPTION_RESERVED, payload, txCtx, {
|
|
1194
|
+
resource: "redemption",
|
|
1195
|
+
resourceId: created._id
|
|
1196
|
+
}), txCtx);
|
|
1197
|
+
return created;
|
|
1030
1198
|
});
|
|
1031
|
-
|
|
1032
|
-
memberId: member._id,
|
|
1033
|
-
externalId: member.externalId,
|
|
1034
|
-
redemptionId: redemption._id,
|
|
1035
|
-
points: validation.pointsToRedeem,
|
|
1036
|
-
discountAmount: validation.discountAmount,
|
|
1037
|
-
ownerType: input.ownerType,
|
|
1038
|
-
ownerId: input.ownerId
|
|
1039
|
-
};
|
|
1040
|
-
await this._dispatch(createEvent(LoyaltyEvents.REDEMPTION_RESERVED, payload, ctx, {
|
|
1041
|
-
resource: "redemption",
|
|
1042
|
-
resourceId: redemption._id
|
|
1043
|
-
}), ctx);
|
|
1044
|
-
this.notifications?.notify(LoyaltyEvents.REDEMPTION_RESERVED, { ...payload });
|
|
1199
|
+
if (payload) this.notifications?.notify(LoyaltyEvents.REDEMPTION_RESERVED, { ...payload });
|
|
1045
1200
|
return redemption;
|
|
1046
1201
|
}
|
|
1047
1202
|
async confirm(redemptionId, ctx) {
|
|
@@ -1054,7 +1209,9 @@ var RedemptionRepository = class extends Repository {
|
|
|
1054
1209
|
});
|
|
1055
1210
|
if (!redemption) throw new RedemptionNotFoundError(redemptionId);
|
|
1056
1211
|
if (redemption.expiresAt && redemption.expiresAt < /* @__PURE__ */ new Date()) throw new RedemptionExpiredError(redemptionId);
|
|
1057
|
-
|
|
1212
|
+
let payload;
|
|
1213
|
+
const updated = await runLoyaltyUnit(this.unitOfWork, this._dispatchDeps, ctx, async (txCtx) => {
|
|
1214
|
+
const session = txCtx.session;
|
|
1058
1215
|
if (!await this.atomicStatusTransition(redemptionId, "reserved", {
|
|
1059
1216
|
status: "confirmed",
|
|
1060
1217
|
pointsConfirmed: redemption.pointsReserved,
|
|
@@ -1063,7 +1220,10 @@ var RedemptionRepository = class extends Repository {
|
|
|
1063
1220
|
session,
|
|
1064
1221
|
...tc
|
|
1065
1222
|
})) throw new ValidationError(`Redemption status must be 'reserved', got '${redemption.status}'`);
|
|
1066
|
-
const member = await this.memberRepo.requireActiveMember(redemption.memberId,
|
|
1223
|
+
const member = await this.memberRepo.requireActiveMember(redemption.memberId, {
|
|
1224
|
+
session,
|
|
1225
|
+
...tc
|
|
1226
|
+
});
|
|
1067
1227
|
const tx = await this.txRepo.create({
|
|
1068
1228
|
memberId: redemption.memberId,
|
|
1069
1229
|
programId: member.programId,
|
|
@@ -1089,26 +1249,22 @@ var RedemptionRepository = class extends Repository {
|
|
|
1089
1249
|
...tc
|
|
1090
1250
|
});
|
|
1091
1251
|
if (!confirmed) throw new RedemptionNotFoundError(redemptionId);
|
|
1252
|
+
payload = {
|
|
1253
|
+
memberId: confirmed.memberId,
|
|
1254
|
+
externalId: member.externalId,
|
|
1255
|
+
redemptionId: confirmed._id,
|
|
1256
|
+
points: confirmed.pointsConfirmed,
|
|
1257
|
+
discountAmount: confirmed.discountAmount,
|
|
1258
|
+
ownerType: confirmed.ownerType,
|
|
1259
|
+
ownerId: confirmed.ownerId
|
|
1260
|
+
};
|
|
1261
|
+
await this._dispatch(createEvent(LoyaltyEvents.REDEMPTION_CONFIRMED, payload, txCtx, {
|
|
1262
|
+
resource: "redemption",
|
|
1263
|
+
resourceId: confirmed._id
|
|
1264
|
+
}), txCtx);
|
|
1092
1265
|
return confirmed;
|
|
1093
1266
|
});
|
|
1094
|
-
|
|
1095
|
-
throwOnNotFound: false,
|
|
1096
|
-
...tc
|
|
1097
|
-
});
|
|
1098
|
-
const payload = {
|
|
1099
|
-
memberId: updated.memberId,
|
|
1100
|
-
externalId: member?.externalId ?? "",
|
|
1101
|
-
redemptionId: updated._id,
|
|
1102
|
-
points: updated.pointsConfirmed,
|
|
1103
|
-
discountAmount: updated.discountAmount,
|
|
1104
|
-
ownerType: updated.ownerType,
|
|
1105
|
-
ownerId: updated.ownerId
|
|
1106
|
-
};
|
|
1107
|
-
await this._dispatch(createEvent(LoyaltyEvents.REDEMPTION_CONFIRMED, payload, ctx, {
|
|
1108
|
-
resource: "redemption",
|
|
1109
|
-
resourceId: updated._id
|
|
1110
|
-
}), ctx);
|
|
1111
|
-
this.notifications?.notify(LoyaltyEvents.REDEMPTION_CONFIRMED, { ...payload });
|
|
1267
|
+
if (payload) this.notifications?.notify(LoyaltyEvents.REDEMPTION_CONFIRMED, { ...payload });
|
|
1112
1268
|
return updated;
|
|
1113
1269
|
}
|
|
1114
1270
|
async release(redemptionId, ctx) {
|
|
@@ -1120,7 +1276,9 @@ var RedemptionRepository = class extends Repository {
|
|
|
1120
1276
|
...tc
|
|
1121
1277
|
});
|
|
1122
1278
|
if (!redemption) throw new RedemptionNotFoundError(redemptionId);
|
|
1123
|
-
|
|
1279
|
+
let payload;
|
|
1280
|
+
const updated = await runLoyaltyUnit(this.unitOfWork, this._dispatchDeps, ctx, async (txCtx) => {
|
|
1281
|
+
const session = txCtx.session;
|
|
1124
1282
|
const transitioned = await this.atomicStatusTransition(redemptionId, "reserved", {
|
|
1125
1283
|
status: "released",
|
|
1126
1284
|
releasedAt: /* @__PURE__ */ new Date()
|
|
@@ -1133,26 +1291,27 @@ var RedemptionRepository = class extends Repository {
|
|
|
1133
1291
|
session,
|
|
1134
1292
|
...tc
|
|
1135
1293
|
});
|
|
1294
|
+
const member = await this.memberRepo.getById(transitioned.memberId, {
|
|
1295
|
+
throwOnNotFound: false,
|
|
1296
|
+
session,
|
|
1297
|
+
...tc
|
|
1298
|
+
});
|
|
1299
|
+
payload = {
|
|
1300
|
+
memberId: transitioned.memberId,
|
|
1301
|
+
externalId: member?.externalId ?? "",
|
|
1302
|
+
redemptionId: transitioned._id,
|
|
1303
|
+
points: transitioned.pointsReserved,
|
|
1304
|
+
discountAmount: transitioned.discountAmount,
|
|
1305
|
+
ownerType: transitioned.ownerType,
|
|
1306
|
+
ownerId: transitioned.ownerId
|
|
1307
|
+
};
|
|
1308
|
+
await this._dispatch(createEvent(LoyaltyEvents.REDEMPTION_RELEASED, payload, txCtx, {
|
|
1309
|
+
resource: "redemption",
|
|
1310
|
+
resourceId: transitioned._id
|
|
1311
|
+
}), txCtx);
|
|
1136
1312
|
return transitioned;
|
|
1137
1313
|
});
|
|
1138
|
-
|
|
1139
|
-
throwOnNotFound: false,
|
|
1140
|
-
...tc
|
|
1141
|
-
});
|
|
1142
|
-
const payload = {
|
|
1143
|
-
memberId: updated.memberId,
|
|
1144
|
-
externalId: member?.externalId ?? "",
|
|
1145
|
-
redemptionId: updated._id,
|
|
1146
|
-
points: updated.pointsReserved,
|
|
1147
|
-
discountAmount: updated.discountAmount,
|
|
1148
|
-
ownerType: updated.ownerType,
|
|
1149
|
-
ownerId: updated.ownerId
|
|
1150
|
-
};
|
|
1151
|
-
await this._dispatch(createEvent(LoyaltyEvents.REDEMPTION_RELEASED, payload, ctx, {
|
|
1152
|
-
resource: "redemption",
|
|
1153
|
-
resourceId: updated._id
|
|
1154
|
-
}), ctx);
|
|
1155
|
-
this.notifications?.notify(LoyaltyEvents.REDEMPTION_RELEASED, { ...payload });
|
|
1314
|
+
if (payload) this.notifications?.notify(LoyaltyEvents.REDEMPTION_RELEASED, { ...payload });
|
|
1156
1315
|
return updated;
|
|
1157
1316
|
}
|
|
1158
1317
|
/**
|
|
@@ -1193,7 +1352,9 @@ var RedemptionRepository = class extends Repository {
|
|
|
1193
1352
|
usedProportional = true;
|
|
1194
1353
|
} else proportionalFallback = true;
|
|
1195
1354
|
}
|
|
1196
|
-
|
|
1355
|
+
let payload;
|
|
1356
|
+
const reversed = await runLoyaltyUnit(this.unitOfWork, this._dispatchDeps, ctx, async (txCtx) => {
|
|
1357
|
+
const session = txCtx.session;
|
|
1197
1358
|
const transitioned = await this.atomicStatusTransition(redemptionId, "confirmed", {
|
|
1198
1359
|
status: "reversed",
|
|
1199
1360
|
reversedAt: /* @__PURE__ */ new Date(),
|
|
@@ -1203,9 +1364,12 @@ var RedemptionRepository = class extends Repository {
|
|
|
1203
1364
|
...tc
|
|
1204
1365
|
});
|
|
1205
1366
|
if (!transitioned) throw new ValidationError(`Redemption status must be 'confirmed', got '${redemption.status}'`);
|
|
1367
|
+
const member = await this.memberRepo.requireMember(redemption.memberId, {
|
|
1368
|
+
session,
|
|
1369
|
+
...tc
|
|
1370
|
+
});
|
|
1206
1371
|
let creditTxId = "";
|
|
1207
1372
|
if (pointsToCredit > 0) {
|
|
1208
|
-
const member = await this.memberRepo.requireMember(redemption.memberId, tc);
|
|
1209
1373
|
const updatedMember = await this.memberRepo.updateBalance(redemption.memberId, {
|
|
1210
1374
|
current: pointsToCredit,
|
|
1211
1375
|
redeemed: -pointsToCredit
|
|
@@ -1238,6 +1402,7 @@ var RedemptionRepository = class extends Repository {
|
|
|
1238
1402
|
}
|
|
1239
1403
|
const finalUpdate = {};
|
|
1240
1404
|
if (creditTxId) finalUpdate.reversalTransactionId = creditTxId;
|
|
1405
|
+
let result = transitioned;
|
|
1241
1406
|
if (Object.keys(finalUpdate).length > 0) {
|
|
1242
1407
|
const updated = await this.update(redemptionId, finalUpdate, {
|
|
1243
1408
|
throwOnNotFound: true,
|
|
@@ -1246,29 +1411,25 @@ var RedemptionRepository = class extends Repository {
|
|
|
1246
1411
|
...tc
|
|
1247
1412
|
});
|
|
1248
1413
|
if (!updated) throw new RedemptionNotFoundError(redemptionId);
|
|
1249
|
-
|
|
1414
|
+
result = updated;
|
|
1250
1415
|
}
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1416
|
+
payload = {
|
|
1417
|
+
memberId: result.memberId,
|
|
1418
|
+
externalId: member.externalId,
|
|
1419
|
+
redemptionId: result._id,
|
|
1420
|
+
pointsCredited: pointsToCredit,
|
|
1421
|
+
originalPoints,
|
|
1422
|
+
proportional: usedProportional,
|
|
1423
|
+
transactionId: result.reversalTransactionId ?? ""
|
|
1424
|
+
};
|
|
1425
|
+
if (options?.reason) payload.reason = options.reason;
|
|
1426
|
+
await this._dispatch(createEvent(LoyaltyEvents.REDEMPTION_REVERSED, payload, txCtx, {
|
|
1427
|
+
resource: "redemption",
|
|
1428
|
+
resourceId: result._id
|
|
1429
|
+
}), txCtx);
|
|
1430
|
+
return result;
|
|
1256
1431
|
});
|
|
1257
|
-
|
|
1258
|
-
memberId: reversed.memberId,
|
|
1259
|
-
externalId: member?.externalId ?? "",
|
|
1260
|
-
redemptionId: reversed._id,
|
|
1261
|
-
pointsCredited: pointsToCredit,
|
|
1262
|
-
originalPoints,
|
|
1263
|
-
proportional: usedProportional,
|
|
1264
|
-
transactionId: reversed.reversalTransactionId ?? ""
|
|
1265
|
-
};
|
|
1266
|
-
if (options?.reason) payload.reason = options.reason;
|
|
1267
|
-
await this._dispatch(createEvent(LoyaltyEvents.REDEMPTION_REVERSED, payload, ctx, {
|
|
1268
|
-
resource: "redemption",
|
|
1269
|
-
resourceId: reversed._id
|
|
1270
|
-
}), ctx);
|
|
1271
|
-
this.notifications?.notify(LoyaltyEvents.REDEMPTION_REVERSED, { ...payload });
|
|
1432
|
+
if (payload) this.notifications?.notify(LoyaltyEvents.REDEMPTION_REVERSED, { ...payload });
|
|
1272
1433
|
return reversed;
|
|
1273
1434
|
}
|
|
1274
1435
|
async cleanupExpired(ctx) {
|
|
@@ -1282,6 +1443,7 @@ var RedemptionRepository = class extends Repository {
|
|
|
1282
1443
|
}, tc);
|
|
1283
1444
|
let released = 0;
|
|
1284
1445
|
for (const redemption of expired) {
|
|
1446
|
+
throwIfAborted(ctx.signal);
|
|
1285
1447
|
await this.release(redemption._id, ctx);
|
|
1286
1448
|
released++;
|
|
1287
1449
|
}
|
|
@@ -1382,8 +1544,7 @@ var ReferralRepository = class extends Repository {
|
|
|
1382
1544
|
...tc
|
|
1383
1545
|
});
|
|
1384
1546
|
if (reverseReferral && String(reverseReferral.referrerId) === String(referee._id)) throw new CircularReferralError();
|
|
1385
|
-
const periodStart = /* @__PURE__ */ new Date();
|
|
1386
|
-
periodStart.setDate(periodStart.getDate() - this.config.referral.periodDays);
|
|
1547
|
+
const periodStart = addDays(/* @__PURE__ */ new Date(), -this.config.referral.periodDays);
|
|
1387
1548
|
if (await this.count({
|
|
1388
1549
|
referrerId: referrer._id,
|
|
1389
1550
|
createdAt: { $gte: periodStart }
|
|
@@ -1483,6 +1644,7 @@ var ReferralRepository = class extends Repository {
|
|
|
1483
1644
|
}, tc);
|
|
1484
1645
|
let count = 0;
|
|
1485
1646
|
for (const referral of expired) {
|
|
1647
|
+
throwIfAborted(ctx.signal);
|
|
1486
1648
|
await this.update(referral._id, { status: "expired" }, tc);
|
|
1487
1649
|
count++;
|
|
1488
1650
|
}
|
|
@@ -1657,16 +1819,19 @@ var TierDefinitionRepository = class extends Repository {
|
|
|
1657
1819
|
let upgraded = 0;
|
|
1658
1820
|
let downgraded = 0;
|
|
1659
1821
|
let unchanged = 0;
|
|
1660
|
-
for (const member of members)
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1822
|
+
for (const member of members) {
|
|
1823
|
+
throwIfAborted(ctx.signal);
|
|
1824
|
+
switch ((await this.evaluateMember(member._id, ctx)).action) {
|
|
1825
|
+
case "upgraded":
|
|
1826
|
+
upgraded++;
|
|
1827
|
+
break;
|
|
1828
|
+
case "downgraded":
|
|
1829
|
+
downgraded++;
|
|
1830
|
+
break;
|
|
1831
|
+
default:
|
|
1832
|
+
unchanged++;
|
|
1833
|
+
break;
|
|
1834
|
+
}
|
|
1670
1835
|
}
|
|
1671
1836
|
return {
|
|
1672
1837
|
upgraded,
|
|
@@ -1745,6 +1910,7 @@ var TierDefinitionRepository = class extends Repository {
|
|
|
1745
1910
|
}, tc);
|
|
1746
1911
|
let cleared = 0;
|
|
1747
1912
|
for (const member of expired) {
|
|
1913
|
+
throwIfAborted(ctx.signal);
|
|
1748
1914
|
const previousOverride = member.tierOverride;
|
|
1749
1915
|
if (!previousOverride) continue;
|
|
1750
1916
|
await this.memberRepo.update(member._id, {
|
|
@@ -1809,8 +1975,7 @@ var TierDefinitionRepository = class extends Repository {
|
|
|
1809
1975
|
action: "unchanged"
|
|
1810
1976
|
};
|
|
1811
1977
|
if (downgrade.gracePeriodDays > 0 && !member.tierExpiresAt) {
|
|
1812
|
-
const graceExpiresAt = /* @__PURE__ */ new Date();
|
|
1813
|
-
graceExpiresAt.setDate(graceExpiresAt.getDate() + downgrade.gracePeriodDays);
|
|
1978
|
+
const graceExpiresAt = addDays(/* @__PURE__ */ new Date(), downgrade.gracePeriodDays);
|
|
1814
1979
|
await this.memberRepo.update(memberId, { tierExpiresAt: graceExpiresAt }, {
|
|
1815
1980
|
lean: true,
|
|
1816
1981
|
...tc
|
|
@@ -1929,4 +2094,4 @@ function createRepositories(input) {
|
|
|
1929
2094
|
};
|
|
1930
2095
|
}
|
|
1931
2096
|
//#endregion
|
|
1932
|
-
export { PointTransactionRepository as a,
|
|
2097
|
+
export { TenantIsolationError as C, ValidationError as E, SelfReferralError as S, UnmanagedSessionError as T, RedemptionExpiredError as _, PointTransactionRepository as a, ReferralNotFoundError as b, flushPendingLoyaltyEvents as c, DuplicateReferralError as d, InsufficientPointsError as f, MemberNotFoundError as g, MemberInactiveError as h, RedemptionRepository as i, EarningRuleRepository as l, MemberAlreadyEnrolledError as m, TierDefinitionRepository as n, MemberRepository as o, LoyaltyCapabilityError as p, ReferralRepository as r, PENDING_EVENTS as s, createRepositories as t, CircularReferralError as u, RedemptionNotFoundError as v, TierNotFoundError as w, RuleNotFoundError as x, ReferralLimitExceededError as y };
|