@classytic/loyalty 0.4.0 → 0.6.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 +106 -0
- package/LICENSE +75 -21
- package/README.md +2 -2
- package/dist/{config-Dtun1duw.d.mts → config-CtkGj3xZ.d.mts} +41 -22
- package/dist/{constants-DkQYkOK1.mjs → constants-DOPBh6kt.mjs} +1 -3
- 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 +2 -2
- package/dist/{events-DN5_p6T0.mjs → events-BLjg3uLA.mjs} +22 -31
- package/dist/{index-Dngaj21_.d.mts → index-CR2k7jQ8.d.mts} +81 -14
- package/dist/{index-YSsi4e_G.d.mts → index-Cp_sXNpG.d.mts} +35 -4
- package/dist/index-D6GvuvmF.d.mts +692 -0
- package/dist/{index-D_5Ojc2c.d.mts → index-YUFamhDk.d.mts} +7 -17
- package/dist/index.d.mts +60 -29
- package/dist/index.mjs +99 -52
- package/dist/models/index.d.mts +3 -1273
- package/dist/models/index.mjs +2 -2
- package/dist/{models-Ddh62lgO.mjs → models-weWqS5Bc.mjs} +152 -70
- package/dist/notifications/index.d.mts +1 -2
- package/dist/repositories/index.d.mts +1 -1
- package/dist/repositories/index.mjs +1 -1
- package/dist/{repositories-DEwCbLCh.mjs → repositories-B676z4bE.mjs} +183 -67
- package/dist/types/index.d.mts +3 -3
- package/package.json +12 -9
- /package/dist/{index-DMK8a98J.d.mts → index-D_Oe3aDk.d.mts} +0 -0
- /package/dist/{loyalty-events-C38JA3cU.mjs → loyalty-events-DXirolM_.mjs} +0 -0
- /package/dist/{notification.port-CJ9nXj0k.d.mts → notification.port-wY4IKimz.d.mts} +0 -0
|
@@ -1,25 +1,14 @@
|
|
|
1
|
-
import { o as LoyaltyContext } from "./index-
|
|
2
|
-
import {
|
|
1
|
+
import { o as LoyaltyContext } from "./index-Cp_sXNpG.mjs";
|
|
2
|
+
import { InProcessEventBus, InProcessEventBusOptions } from "@classytic/primitives/event-infra";
|
|
3
|
+
import { DomainEvent, DomainEvent as DomainEvent$1, EventHandler, EventTransport as EventTransport$1 } from "@classytic/primitives/events";
|
|
3
4
|
import { z } from "zod";
|
|
4
|
-
|
|
5
5
|
//#region src/events/helpers.d.ts
|
|
6
6
|
declare function createEvent$1<T>(type: string, payload: T, ctx?: LoyaltyContext, meta?: Partial<DomainEvent['meta']>): DomainEvent<T>;
|
|
7
7
|
//#endregion
|
|
8
8
|
//#region src/events/in-process-bus.d.ts
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
logger?: {
|
|
12
|
-
error: (msg: string, ...args: unknown[]) => void;
|
|
13
|
-
};
|
|
14
|
-
}
|
|
15
|
-
declare class InProcessLoyaltyBus implements EventTransport {
|
|
16
|
-
readonly name = "in-process-loyalty";
|
|
17
|
-
private handlers;
|
|
18
|
-
private logger;
|
|
9
|
+
type InProcessLoyaltyBusOptions = InProcessEventBusOptions;
|
|
10
|
+
declare class InProcessLoyaltyBus extends InProcessEventBus {
|
|
19
11
|
constructor(options?: InProcessLoyaltyBusOptions);
|
|
20
|
-
publish(event: DomainEvent): Promise<void>;
|
|
21
|
-
subscribe(pattern: string, handler: EventHandler): Promise<() => void>;
|
|
22
|
-
close(): Promise<void>;
|
|
23
12
|
}
|
|
24
13
|
//#endregion
|
|
25
14
|
//#region src/events/loyalty-event-catalog.d.ts
|
|
@@ -318,6 +307,7 @@ interface RedemptionPayload {
|
|
|
318
307
|
externalId: string;
|
|
319
308
|
redemptionId: string;
|
|
320
309
|
points: number;
|
|
310
|
+
/** Discount in integer minor units (paisa for BDT). */
|
|
321
311
|
discountAmount: number;
|
|
322
312
|
ownerType: string;
|
|
323
313
|
ownerId: string;
|
|
@@ -397,4 +387,4 @@ interface LoyaltyEventMap {
|
|
|
397
387
|
}
|
|
398
388
|
type LoyaltyEventPayload<E extends LoyaltyEventName> = LoyaltyEventMap[E];
|
|
399
389
|
//#endregion
|
|
400
|
-
export { PointsExpired as A, ReferralRewarded as B, MemberDeactivated as C, MultiTierReferralAwarded as D, MemberSuspended as E, RedemptionReserved as F, TierUpgraded as G, TierGraceStarted as H, RedemptionReversed as I, InProcessLoyaltyBusOptions as J, loyaltyEventDefinitions as K, ReferralApproved as L, PointsRedeemed as M, RedemptionConfirmed as N, PointsAdjusted as O, RedemptionReleased as P, ReferralRecorded as R, LoyaltyEventSchema as S, MemberReactivated as T, TierOverrideCleared as U, TierDowngraded as V, TierOverrideExpired as W, createEvent$1 as Y, TierChangedPayload as _, LoyaltyEventMap as a, LoyaltyEventDefinition as b, LoyaltyEvents as c, PointsAdjustedPayload as d, PointsEarnedPayload as f, ReferralPayload as g, RedemptionReversedPayload as h, CampaignAwardedPayload as i, PointsExpiringSoon as j, PointsEarned as k, MemberLifecyclePayload as l, RedemptionPayload as m, EventHandler
|
|
390
|
+
export { PointsExpired as A, ReferralRewarded as B, MemberDeactivated as C, MultiTierReferralAwarded as D, MemberSuspended as E, RedemptionReserved as F, TierUpgraded as G, TierGraceStarted as H, RedemptionReversed as I, InProcessLoyaltyBusOptions as J, loyaltyEventDefinitions as K, ReferralApproved as L, PointsRedeemed as M, RedemptionConfirmed as N, PointsAdjusted as O, RedemptionReleased as P, ReferralRecorded as R, LoyaltyEventSchema as S, MemberReactivated as T, TierOverrideCleared as U, TierDowngraded as V, TierOverrideExpired as W, createEvent$1 as Y, TierChangedPayload as _, LoyaltyEventMap as a, LoyaltyEventDefinition as b, LoyaltyEvents as c, PointsAdjustedPayload as d, PointsEarnedPayload as f, ReferralPayload as g, RedemptionReversedPayload as h, CampaignAwardedPayload as i, PointsExpiringSoon as j, PointsEarned as k, MemberLifecyclePayload as l, RedemptionPayload as m, EventHandler as n, LoyaltyEventName as o, PointsExpiredPayload as p, InProcessLoyaltyBus as q, EventTransport$1 as r, LoyaltyEventPayload as s, DomainEvent$1 as t, MultiTierReferralAwardedPayload as u, TierOverrideLifecyclePayload as v, MemberEnrolled as w, LoyaltyEventPayloadOf as x, CampaignAwarded as y, ReferralRejected as z };
|
package/dist/index.d.mts
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
|
-
import { S as EarningRule, a as EnrollInput, b as MemberBalance, c as UpdateTierInput, d as EarningResult, g as TierEvaluation, h as RedemptionValidation, i as EarnPointsInput, l as BalanceSummary, n as CreateEarningRuleInput, o as LoyaltyContext, r as CreateTierInput, s as UpdateEarningRuleInput, t as AdjustPointsInput, v as PointTransaction, x as TierDefinition, y as Member } from "./index-
|
|
2
|
-
import { t as NotificationAdapter } from "./notification.port-
|
|
3
|
-
import { c as
|
|
4
|
-
import { A as PointsExpired, B as ReferralRewarded, C as MemberDeactivated, E as MemberSuspended, F as RedemptionReserved, G as TierUpgraded, H as TierGraceStarted, J as InProcessLoyaltyBusOptions, K as loyaltyEventDefinitions, L as ReferralApproved, M as PointsRedeemed, N as RedemptionConfirmed, O as PointsAdjusted, P as RedemptionReleased, R as ReferralRecorded, S as LoyaltyEventSchema, T as MemberReactivated, V as TierDowngraded, Y as createEvent, _ as TierChangedPayload, a as LoyaltyEventMap, b as LoyaltyEventDefinition, c as LoyaltyEvents, d as PointsAdjustedPayload, f as PointsEarnedPayload, g as ReferralPayload, j as PointsExpiringSoon, k as PointsEarned, l as MemberLifecyclePayload, m as RedemptionPayload, o as LoyaltyEventName, p as PointsExpiredPayload, q as InProcessLoyaltyBus, s as LoyaltyEventPayload, w as MemberEnrolled, x as LoyaltyEventPayloadOf, z as ReferralRejected } from "./index-
|
|
5
|
-
import { LoyaltyModels } from "./
|
|
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-
|
|
1
|
+
import { S as EarningRule, a as EnrollInput, b as MemberBalance, c as UpdateTierInput, d as EarningResult, g as TierEvaluation, h as RedemptionValidation, i as EarnPointsInput, l as BalanceSummary, n as CreateEarningRuleInput, o as LoyaltyContext, r as CreateTierInput, s as UpdateEarningRuleInput, t as AdjustPointsInput, v as PointTransaction, x as TierDefinition, y as Member } from "./index-Cp_sXNpG.mjs";
|
|
2
|
+
import { t as NotificationAdapter } from "./notification.port-wY4IKimz.mjs";
|
|
3
|
+
import { c as ResolvedConfig, i as LoyaltyShape, l as ResolvedTenant, n as LoyaltyModelName, r as LoyaltyRuntime } from "./config-CtkGj3xZ.mjs";
|
|
4
|
+
import { A as PointsExpired, B as ReferralRewarded, C as MemberDeactivated, E as MemberSuspended, F as RedemptionReserved, G as TierUpgraded, H as TierGraceStarted, J as InProcessLoyaltyBusOptions, K as loyaltyEventDefinitions, L as ReferralApproved, M as PointsRedeemed, N as RedemptionConfirmed, O as PointsAdjusted, P as RedemptionReleased, R as ReferralRecorded, S as LoyaltyEventSchema, T as MemberReactivated, V as TierDowngraded, Y as createEvent, _ as TierChangedPayload, a as LoyaltyEventMap, b as LoyaltyEventDefinition, c as LoyaltyEvents, d as PointsAdjustedPayload, f as PointsEarnedPayload, g as ReferralPayload, j as PointsExpiringSoon, k as PointsEarned, l as MemberLifecyclePayload, m as RedemptionPayload, o as LoyaltyEventName, p as PointsExpiredPayload, q as InProcessLoyaltyBus, s as LoyaltyEventPayload, w as MemberEnrolled, x as LoyaltyEventPayloadOf, z as ReferralRejected } from "./index-YUFamhDk.mjs";
|
|
5
|
+
import { n as LoyaltyModels, r as defineLoyaltyModels, t as DefineLoyaltyModelsConfig } from "./index-D6GvuvmF.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-CR2k7jQ8.mjs";
|
|
7
|
+
import { Connection } from "mongoose";
|
|
7
8
|
import { DomainEvent, DomainEvent as DomainEvent$1, EventHandler, EventTransport, EventTransport as EventTransport$1 } from "@classytic/primitives/events";
|
|
8
9
|
import { OutboxStore } from "@classytic/primitives/outbox";
|
|
9
10
|
import { HttpError } from "@classytic/repo-core/errors";
|
|
10
|
-
|
|
11
11
|
//#region src/domain/errors/base.d.ts
|
|
12
12
|
/**
|
|
13
13
|
* Base for every loyalty throwable. Implements `HttpError` from
|
|
@@ -122,6 +122,25 @@ declare class UnmanagedSessionError extends LoyaltyError {
|
|
|
122
122
|
readonly status = 400;
|
|
123
123
|
constructor();
|
|
124
124
|
}
|
|
125
|
+
/**
|
|
126
|
+
* A host passed `ctx.session`, but that session has no ACTIVE transaction.
|
|
127
|
+
*
|
|
128
|
+
* A bare `startSession()` provides NO atomicity — every write inside it
|
|
129
|
+
* commits independently. Accepting it as proof of a unit of work is the
|
|
130
|
+
* silent-permissiveness failure in its purest form: loyalty then saves the
|
|
131
|
+
* outbox row "in-session" and applies the balance `$inc` "in-session", and
|
|
132
|
+
* if the outbox save fails the balance mutation still stands with no ledger
|
|
133
|
+
* row behind it. Nothing throws; the points are simply wrong.
|
|
134
|
+
*
|
|
135
|
+
* `session.inTransaction()` is the driver's own answer, so this check cannot
|
|
136
|
+
* drift from what MongoDB actually thinks. `@classytic/access` refuses
|
|
137
|
+
* exactly this at `entitlement.repository.ts` — same rule, same reason.
|
|
138
|
+
*/
|
|
139
|
+
declare class SessionNotInTransactionError extends LoyaltyError {
|
|
140
|
+
readonly code = "loyalty.transaction.session_not_in_transaction";
|
|
141
|
+
readonly status = 409;
|
|
142
|
+
constructor();
|
|
143
|
+
}
|
|
125
144
|
/**
|
|
126
145
|
* The backend doesn't declare the runtime capabilities loyalty's
|
|
127
146
|
* transactional verbs require (earn / adjust / reserve / confirm / release /
|
|
@@ -132,7 +151,16 @@ declare class UnmanagedSessionError extends LoyaltyError {
|
|
|
132
151
|
declare class LoyaltyCapabilityError extends LoyaltyError {
|
|
133
152
|
readonly code = "loyalty.engine.missing_capabilities";
|
|
134
153
|
readonly status = 500;
|
|
135
|
-
|
|
154
|
+
/**
|
|
155
|
+
* `true` when the backend never ANSWERED (bound before the connection
|
|
156
|
+
* opened, a driver exposing no topology) rather than answering "no". Both
|
|
157
|
+
* refuse — an unobserved outcome is never a positive one (AGENTS.md FAIL
|
|
158
|
+
* LOUD rule 3) — but the operator's fix differs, so the message must.
|
|
159
|
+
*/
|
|
160
|
+
readonly indeterminate: boolean;
|
|
161
|
+
constructor(missing: readonly string[], opts?: {
|
|
162
|
+
indeterminate?: boolean;
|
|
163
|
+
});
|
|
136
164
|
}
|
|
137
165
|
//#endregion
|
|
138
166
|
//#region src/events/dispatch.d.ts
|
|
@@ -155,31 +183,34 @@ declare function flushPendingLoyaltyEvents(events: EventTransport$1 | undefined,
|
|
|
155
183
|
} | undefined): Promise<void>;
|
|
156
184
|
//#endregion
|
|
157
185
|
//#region src/index.d.ts
|
|
158
|
-
declare function resolveConfig(config:
|
|
159
|
-
/**
|
|
160
|
-
* LoyaltyEngine -- repositories + arc-compatible event transport.
|
|
161
|
-
*
|
|
162
|
-
* Domain verbs live on the repositories themselves (`engine.repositories.member.enroll(...)`,
|
|
163
|
-
* `engine.repositories.pointTransaction.earnPoints(...)`, etc.). There is no separate
|
|
164
|
-
* service layer. See PACKAGE_RULES S1-S3.
|
|
165
|
-
*
|
|
166
|
-
* `events` is structurally identical to `@classytic/arc`'s `EventTransport`.
|
|
167
|
-
* Hosts can pass any arc transport (Memory/Redis/Kafka) via `eventTransport`
|
|
168
|
-
* and subscribe glob-style:
|
|
169
|
-
*
|
|
170
|
-
* const loyalty = createLoyaltyEngine({
|
|
171
|
-
* mongoose,
|
|
172
|
-
* eventTransport: new RedisEventTransport({ url }),
|
|
173
|
-
* });
|
|
174
|
-
*
|
|
175
|
-
* await loyalty.events.subscribe('loyalty.points.*', handler);
|
|
176
|
-
*/
|
|
186
|
+
declare function resolveConfig(config: LoyaltyShape): ResolvedConfig;
|
|
177
187
|
interface LoyaltyEngine {
|
|
178
188
|
models: LoyaltyModels;
|
|
179
189
|
repositories: LoyaltyRepositories;
|
|
180
190
|
events: EventTransport$1;
|
|
191
|
+
/** Build/align indexes. Call once per deploy when booted `autoIndex: false`. */
|
|
181
192
|
syncIndexes(): Promise<void>;
|
|
193
|
+
/**
|
|
194
|
+
* Release resources owned by this engine. Closes the event bus ONLY when the
|
|
195
|
+
* engine created it itself (`InProcessLoyaltyBus`); a host-injected
|
|
196
|
+
* `eventTransport` is the host's to close. Idempotent.
|
|
197
|
+
*/
|
|
198
|
+
close(): Promise<void>;
|
|
199
|
+
}
|
|
200
|
+
interface LoyaltyBlueprint {
|
|
201
|
+
readonly id: 'loyalty';
|
|
202
|
+
/** The model names this blueprint registers — known without a connection. */
|
|
203
|
+
readonly modelNames: readonly string[];
|
|
204
|
+
/**
|
|
205
|
+
* Compile + register the models on `connection`, wire the engine, verify
|
|
206
|
+
* capabilities. Synchronous: loyalty's construction performs no async work.
|
|
207
|
+
*/
|
|
208
|
+
bind(connection: Connection, runtime?: LoyaltyRuntime): LoyaltyEngine;
|
|
182
209
|
}
|
|
183
|
-
|
|
210
|
+
/**
|
|
211
|
+
* Describe the loyalty engine. Pure — registers no model, builds no schema,
|
|
212
|
+
* performs no I/O. Config is resolved and validated here.
|
|
213
|
+
*/
|
|
214
|
+
declare function defineLoyalty(shape?: LoyaltyShape): LoyaltyBlueprint;
|
|
184
215
|
//#endregion
|
|
185
|
-
export { type AdjustPointsInput, type BalanceSummary, CircularReferralError, type CreateEarningRuleInput, type CreateTierInput, type DomainEvent, DuplicateReferralError, type EarnPointsInput, type EarningResult, type EarningRule, EarningRuleRepository, type EnrollInput, type EventHandler, type EventTransport, InProcessLoyaltyBus, type InProcessLoyaltyBusOptions, InsufficientPointsError,
|
|
216
|
+
export { type AdjustPointsInput, type BalanceSummary, CircularReferralError, type CreateEarningRuleInput, type CreateTierInput, type DefineLoyaltyModelsConfig, type DomainEvent, DuplicateReferralError, type EarnPointsInput, type EarningResult, type EarningRule, EarningRuleRepository, type EnrollInput, type EventHandler, type EventTransport, InProcessLoyaltyBus, type InProcessLoyaltyBusOptions, InsufficientPointsError, LoyaltyBlueprint, LoyaltyCapabilityError, type LoyaltyContext, LoyaltyEngine, type LoyaltyEventDefinition, type LoyaltyEventMap, type LoyaltyEventName, type LoyaltyEventPayload, type LoyaltyEventPayloadOf, type LoyaltyEventSchema, LoyaltyEvents, type LoyaltyModelName, type LoyaltyModels, type LoyaltyRepositories, type LoyaltyRuntime, type LoyaltyShape, type Member, MemberAlreadyEnrolledError, type MemberBalance, MemberDeactivated, MemberEnrolled, MemberInactiveError, type MemberLifecyclePayload, MemberNotFoundError, MemberReactivated, MemberRepository, MemberSuspended, type NotificationAdapter, PENDING_EVENTS, type PointTransaction, PointTransactionRepository, PointsAdjusted, type PointsAdjustedPayload, PointsEarned, type PointsEarnedPayload, PointsExpired, type PointsExpiredPayload, PointsExpiringSoon, PointsRedeemed, type Redemption, RedemptionConfirmed, RedemptionExpiredError, RedemptionNotFoundError, type RedemptionPayload, RedemptionReleased, RedemptionRepository, RedemptionReserved, type RedemptionValidation, type Referral, ReferralApproved, ReferralLimitExceededError, ReferralNotFoundError, type ReferralPayload, ReferralRecorded, ReferralRejected, ReferralRepository, ReferralRewarded, type ResolvedConfig, type ResolvedTenant, RuleNotFoundError, SelfReferralError, SessionNotInTransactionError, TenantIsolationError, type TierChangedPayload, type TierDefinition, TierDefinitionRepository, TierDowngraded, type TierEvaluation, TierGraceStarted, TierNotFoundError, TierUpgraded, type UnitOfWork, UnmanagedSessionError, type UpdateEarningRuleInput, type UpdateTierInput, ValidationError, createEvent, defineLoyalty, defineLoyaltyModels, flushPendingLoyaltyEvents, loyaltyEventDefinitions, resolveConfig };
|
package/dist/index.mjs
CHANGED
|
@@ -1,35 +1,18 @@
|
|
|
1
|
-
import
|
|
2
|
-
import { C as
|
|
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-
|
|
4
|
-
import {
|
|
5
|
-
import { n as createEvent, t as LoyaltyEvents } from "./loyalty-events-
|
|
6
|
-
import { withTransaction } from "@classytic/mongokit";
|
|
1
|
+
import "./constants-DOPBh6kt.mjs";
|
|
2
|
+
import { C as SessionNotInTransactionError, D as ValidationError, E as UnmanagedSessionError, S as SelfReferralError, T as TierNotFoundError, _ as RedemptionExpiredError, a as PointTransactionRepository, b as ReferralNotFoundError, c as flushPendingLoyaltyEvents, d as DuplicateReferralError, f as InsufficientPointsError, g as MemberNotFoundError, h as MemberInactiveError, i as RedemptionRepository, l as EarningRuleRepository, m as MemberAlreadyEnrolledError, n as TierDefinitionRepository, o as MemberRepository, p as LoyaltyCapabilityError, r as ReferralRepository, s as PENDING_EVENTS, t as createRepositories, u as CircularReferralError, v as RedemptionNotFoundError, w as TenantIsolationError, x as RuleNotFoundError, y as ReferralLimitExceededError } from "./repositories-B676z4bE.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-BLjg3uLA.mjs";
|
|
4
|
+
import { t as defineLoyaltyModels } from "./models-weWqS5Bc.mjs";
|
|
5
|
+
import { n as createEvent, t as LoyaltyEvents } from "./loyalty-events-DXirolM_.mjs";
|
|
6
|
+
import { transactionResolutionOf, withTransaction } from "@classytic/mongokit";
|
|
7
7
|
import { isValidTimeZone } from "@classytic/primitives/timezone";
|
|
8
|
+
import { assertNoLegacyTenantKeys, resolveTenantConfig } from "@classytic/repo-core/tenant";
|
|
8
9
|
//#region src/index.ts
|
|
9
10
|
function resolveConfig(config) {
|
|
10
|
-
|
|
11
|
-
if (
|
|
12
|
-
enabled: false,
|
|
13
|
-
tenantField: DEFAULT_TENANT_FIELD,
|
|
14
|
-
fieldType: DEFAULT_TENANT_TYPE,
|
|
15
|
-
contextKey: DEFAULT_TENANT_FIELD
|
|
16
|
-
};
|
|
17
|
-
else if (config.tenant === void 0) tenant = {
|
|
18
|
-
enabled: true,
|
|
19
|
-
tenantField: DEFAULT_TENANT_FIELD,
|
|
20
|
-
fieldType: DEFAULT_TENANT_TYPE,
|
|
21
|
-
contextKey: DEFAULT_TENANT_FIELD
|
|
22
|
-
};
|
|
23
|
-
else tenant = {
|
|
24
|
-
enabled: true,
|
|
25
|
-
tenantField: config.tenant.tenantField ?? "organizationId",
|
|
26
|
-
fieldType: config.tenant.fieldType ?? "objectId",
|
|
27
|
-
contextKey: config.tenant.contextKey ?? config.tenant.tenantField ?? "organizationId",
|
|
28
|
-
...config.tenant.ref ? { ref: config.tenant.ref } : {}
|
|
29
|
-
};
|
|
11
|
+
const tenant = resolveTenantConfig(config.tenant);
|
|
12
|
+
if (tenant.strategy === "custom") throw new Error("[loyalty] tenant.strategy 'custom' is unsupported: loyalty repositories require one concrete tenant field");
|
|
30
13
|
const resolved = {
|
|
31
14
|
programId: config.program?.id ?? "default",
|
|
32
|
-
|
|
15
|
+
pointValueMinor: config.program?.pointValueMinor ?? 10,
|
|
33
16
|
redemption: {
|
|
34
17
|
minPoints: config.redemption?.minPoints ?? 0,
|
|
35
18
|
minOrderAmount: config.redemption?.minOrderAmount ?? 0,
|
|
@@ -54,12 +37,14 @@ function resolveConfig(config) {
|
|
|
54
37
|
tenant
|
|
55
38
|
};
|
|
56
39
|
if (config.timezone !== void 0 && !isValidTimeZone(config.timezone)) throw new ValidationError(`Invalid IANA timezone: '${config.timezone}'`);
|
|
57
|
-
if (resolved.
|
|
40
|
+
if (!Number.isSafeInteger(resolved.pointValueMinor) || resolved.pointValueMinor <= 0) throw new ValidationError("pointValueMinor must be a positive integer (minor units per point)");
|
|
58
41
|
if (resolved.referral.codeLength < 4) throw new ValidationError("referral codeLength must be >= 4");
|
|
59
42
|
if (resolved.redemption.maxRedeemPercent < 0 || resolved.redemption.maxRedeemPercent > 100) throw new ValidationError("maxRedeemPercent must be between 0 and 100");
|
|
60
43
|
return resolved;
|
|
61
44
|
}
|
|
62
45
|
var MongoUnitOfWork = class {
|
|
46
|
+
connection;
|
|
47
|
+
allowFallback;
|
|
63
48
|
constructor(connection, allowFallback) {
|
|
64
49
|
this.connection = connection;
|
|
65
50
|
this.allowFallback = allowFallback;
|
|
@@ -68,34 +53,96 @@ var MongoUnitOfWork = class {
|
|
|
68
53
|
return withTransaction(this.connection, cb, this.allowFallback ? { allowFallback: true } : {});
|
|
69
54
|
}
|
|
70
55
|
};
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
56
|
+
/**
|
|
57
|
+
* Describe the loyalty engine. Pure — registers no model, builds no schema,
|
|
58
|
+
* performs no I/O. Config is resolved and validated here.
|
|
59
|
+
*/
|
|
60
|
+
function defineLoyalty(shape = {}) {
|
|
61
|
+
assertNoLegacyTenantKeys(shape, "defineLoyalty", [["tenantFieldType", "tenant.fieldType"]]);
|
|
62
|
+
const resolvedConfig = resolveConfig(shape);
|
|
63
|
+
const modelBlueprint = defineLoyaltyModels({
|
|
79
64
|
tenant: resolvedConfig.tenant,
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
unitOfWork
|
|
65
|
+
indexes: shape.indexes,
|
|
66
|
+
autoIndex: shape.autoIndex,
|
|
67
|
+
collectionPrefix: shape.collectionPrefix,
|
|
68
|
+
forceRecreate: shape.forceRecreate
|
|
85
69
|
});
|
|
86
|
-
if (!config.allowNonTransactional) {
|
|
87
|
-
const caps = repositories.pointTransaction.capabilities;
|
|
88
|
-
if (!caps) throw new LoyaltyCapabilityError(["capabilities descriptor (repo-core 0.6 / mongokit 3.16+) — backend declares none"]);
|
|
89
|
-
if (caps.transactions !== true) throw new LoyaltyCapabilityError(["transactions"]);
|
|
90
|
-
}
|
|
91
70
|
return {
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
71
|
+
id: "loyalty",
|
|
72
|
+
modelNames: modelBlueprint.modelNames,
|
|
73
|
+
bind(connection, runtime = {}) {
|
|
74
|
+
/**
|
|
75
|
+
* VERIFY (boot gate) — an injected outbox MUST enlist `ctx.session`.
|
|
76
|
+
*
|
|
77
|
+
* Checked FIRST, before the model blueprint binds: a cheaper error should report
|
|
78
|
+
* before a costlier one, and a bind that throws here must not have already mutated
|
|
79
|
+
* the connection's model registry.
|
|
80
|
+
*
|
|
81
|
+
* `OutboxWriteOptions.session` is best-effort BY CONTRACT (see
|
|
82
|
+
* `@classytic/primitives/outbox`) — "stores may ignore fields they don't support".
|
|
83
|
+
* A hand-rolled host store therefore satisfies `OutboxStore` structurally, compiles,
|
|
84
|
+
* runs, and persists the event row OUTSIDE the caller's transaction.
|
|
85
|
+
*
|
|
86
|
+
* Points are STORED VALUE, so that lands on the worst path this kernel has.
|
|
87
|
+
* `earn` / `redeem` / `reverse` `$inc` the member balance and insert the ledger row
|
|
88
|
+
* in ONE transaction while `dispatchLoyaltyEvent` saves the event in-session
|
|
89
|
+
* (`events/dispatch.ts`), so the row must vanish with them on rollback. A store that
|
|
90
|
+
* drops the session emits `loyalty.points.earned` for a balance that rolled back —
|
|
91
|
+
* a tier recalculation or a notification then credits a customer with points the
|
|
92
|
+
* ledger never recorded. Nothing throws.
|
|
93
|
+
*
|
|
94
|
+
* `dispatch.ts` treats an outbox as licence for "durable-relay-only mode" (a bare
|
|
95
|
+
* host session with no PENDING_EVENTS queue is legal *because* the row is assumed to
|
|
96
|
+
* commit with the host transaction) — that assumption is exactly what this verifies.
|
|
97
|
+
* `UnmanagedSessionError` guards the OPPOSITE hole (session, NO outbox) and cannot
|
|
98
|
+
* see this one: the store is present and `save()` resolves.
|
|
99
|
+
*
|
|
100
|
+
* NOT waived by `allowNonTransactional` — that opts into standalone-Mongo partial
|
|
101
|
+
* writes, a different requirement entirely.
|
|
102
|
+
*
|
|
103
|
+
* `!== true`, not `=== false`: a hand-rolled store has the flag ABSENT.
|
|
104
|
+
*/
|
|
105
|
+
if (runtime.outbox && runtime.outbox.transactionalSave !== true) throw new LoyaltyCapabilityError(["outbox.transactionalSave (the configured OutboxStore does not declare that save() enlists ctx.session — see @classytic/primitives/outbox)"]);
|
|
106
|
+
const models = modelBlueprint.bind(connection);
|
|
107
|
+
const allowNonTransactional = runtime.allowNonTransactional ?? false;
|
|
108
|
+
const unitOfWork = new MongoUnitOfWork(connection, allowNonTransactional);
|
|
109
|
+
const ownsBus = runtime.eventTransport === void 0;
|
|
110
|
+
const events = runtime.eventTransport ?? new InProcessLoyaltyBus();
|
|
111
|
+
const repositories = createRepositories({
|
|
112
|
+
models,
|
|
113
|
+
plugins: runtime.plugins,
|
|
114
|
+
tenant: resolvedConfig.tenant,
|
|
115
|
+
config: resolvedConfig,
|
|
116
|
+
events,
|
|
117
|
+
outbox: runtime.outbox,
|
|
118
|
+
notifications: runtime.notifications ?? null,
|
|
119
|
+
unitOfWork
|
|
120
|
+
});
|
|
121
|
+
{
|
|
122
|
+
const caps = repositories.pointTransaction.capabilities;
|
|
123
|
+
if (!caps) throw new LoyaltyCapabilityError(["capabilities descriptor (repo-core 0.6 / mongokit 3.16+) — backend declares none"]);
|
|
124
|
+
if (caps.transactions !== true) {
|
|
125
|
+
const indeterminate = transactionResolutionOf(caps) === "unknown";
|
|
126
|
+
if (allowNonTransactional) (runtime.logger ?? console).warn?.(`[loyalty] the backend ${indeterminate ? "could NOT be confirmed to support" : "does NOT support"} transactions. allowNonTransactional=true: point earn / redemption / reversal run WITHOUT atomicity — a crash mid-verb can leave a balance no transaction explains. Development only.`);
|
|
127
|
+
else throw new LoyaltyCapabilityError(["transactions"], { indeterminate });
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
let closed = false;
|
|
131
|
+
return {
|
|
132
|
+
models,
|
|
133
|
+
repositories,
|
|
134
|
+
events,
|
|
135
|
+
async syncIndexes() {
|
|
136
|
+
await Promise.all(Object.values(models).map((m) => m.createIndexes()));
|
|
137
|
+
},
|
|
138
|
+
async close() {
|
|
139
|
+
if (closed) return;
|
|
140
|
+
closed = true;
|
|
141
|
+
if (ownsBus) await events.close?.();
|
|
142
|
+
}
|
|
143
|
+
};
|
|
97
144
|
}
|
|
98
145
|
};
|
|
99
146
|
}
|
|
100
147
|
//#endregion
|
|
101
|
-
export { CircularReferralError, DuplicateReferralError, EarningRuleRepository, InProcessLoyaltyBus, InsufficientPointsError, LoyaltyCapabilityError, LoyaltyEvents, MemberAlreadyEnrolledError, MemberDeactivated, MemberEnrolled, MemberInactiveError, MemberNotFoundError, MemberReactivated, MemberRepository, MemberSuspended, PENDING_EVENTS, PointTransactionRepository, PointsAdjusted, PointsEarned, PointsExpired, PointsExpiringSoon, PointsRedeemed, RedemptionConfirmed, RedemptionExpiredError, RedemptionNotFoundError, RedemptionReleased, RedemptionRepository, RedemptionReserved, ReferralApproved, ReferralLimitExceededError, ReferralNotFoundError, ReferralRecorded, ReferralRejected, ReferralRepository, ReferralRewarded, RuleNotFoundError, SelfReferralError, TenantIsolationError, TierDefinitionRepository, TierDowngraded, TierGraceStarted, TierNotFoundError, TierUpgraded, UnmanagedSessionError, ValidationError, createEvent,
|
|
148
|
+
export { CircularReferralError, DuplicateReferralError, EarningRuleRepository, InProcessLoyaltyBus, InsufficientPointsError, LoyaltyCapabilityError, LoyaltyEvents, MemberAlreadyEnrolledError, MemberDeactivated, MemberEnrolled, MemberInactiveError, MemberNotFoundError, MemberReactivated, MemberRepository, MemberSuspended, PENDING_EVENTS, PointTransactionRepository, PointsAdjusted, PointsEarned, PointsExpired, PointsExpiringSoon, PointsRedeemed, RedemptionConfirmed, RedemptionExpiredError, RedemptionNotFoundError, RedemptionReleased, RedemptionRepository, RedemptionReserved, ReferralApproved, ReferralLimitExceededError, ReferralNotFoundError, ReferralRecorded, ReferralRejected, ReferralRepository, ReferralRewarded, RuleNotFoundError, SelfReferralError, SessionNotInTransactionError, TenantIsolationError, TierDefinitionRepository, TierDowngraded, TierGraceStarted, TierNotFoundError, TierUpgraded, UnmanagedSessionError, ValidationError, createEvent, defineLoyalty, defineLoyaltyModels, flushPendingLoyaltyEvents, loyaltyEventDefinitions, resolveConfig };
|