@classytic/loyalty 0.1.0 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +68 -0
- package/dist/config-DoZPGBfW.d.mts +136 -0
- package/dist/{constants-C75GvizO.mjs → constants-DkQYkOK1.mjs} +3 -2
- package/dist/domain/enums/index.d.mts +1 -1
- package/dist/domain/enums/index.mjs +1 -1
- package/dist/events/index.d.mts +2 -2
- package/dist/events/index.mjs +3 -3
- package/dist/events-BtZOdiPw.mjs +311 -0
- package/dist/index-BOWqBSWx.d.mts +281 -0
- package/dist/index-C4jFHS7m.d.mts +400 -0
- package/dist/index-ClPKmtxh.d.mts +361 -0
- package/dist/{index-CaH4GhSF.d.mts → index-DMK8a98J.d.mts} +1 -1
- package/dist/index.d.mts +28 -12
- package/dist/index.mjs +30 -26
- package/dist/loyalty-events-Cyt9TwX6.mjs +50 -0
- package/dist/models/index.d.mts +1225 -2
- package/dist/models/index.mjs +2 -2
- package/dist/{models-CrZPmVxt.mjs → models-uhh9c-7k.mjs} +104 -41
- package/dist/notifications/index.d.mts +1 -1
- package/dist/repositories/index.d.mts +2 -2
- package/dist/repositories/index.mjs +1 -1
- package/dist/repositories-DBSxe9st.mjs +1919 -0
- package/dist/types/index.d.mts +2 -2
- package/package.json +22 -17
- package/dist/config-aNigy4ec.d.mts +0 -90
- package/dist/domain/index.d.mts +0 -90
- package/dist/domain/index.mjs +0 -4
- package/dist/domain-errors-D7q-2LYD.mjs +0 -120
- package/dist/event-emitter.port-CLYPoMni.d.mts +0 -8
- package/dist/event-types-xQBtYBbX.mjs +0 -24
- package/dist/events-CprEWlN7.mjs +0 -25
- package/dist/index-B6ZstxSP.d.mts +0 -2357
- package/dist/index-BcoGeDco.d.mts +0 -127
- package/dist/index-DqzTjeEB.d.mts +0 -148
- package/dist/index-O7TL4R2P.d.mts +0 -86
- package/dist/index-_pg4J11t.d.mts +0 -191
- package/dist/repositories-qMX7Xh_y.mjs +0 -440
- package/dist/services/index.d.mts +0 -2
- package/dist/services/index.mjs +0 -2
- package/dist/services-CoTqC4E6.mjs +0 -1098
- package/dist/tier-definition-Djf72UPa.d.mts +0 -120
- package/dist/tier-definition.port-BzrHYBRo.d.mts +0 -143
- /package/dist/{notification.port-BzUsH0It.d.mts → notification.port-CJ9nXj0k.d.mts} +0 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
Format based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
4
|
+
adhering to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
5
|
+
|
|
6
|
+
## [0.2.0]
|
|
7
|
+
|
|
8
|
+
Structural rewrite on top of the 0.1.0 shape. The public API is now a
|
|
9
|
+
smaller, sharper surface organized around seven subpath exports
|
|
10
|
+
(`/domain/enums`, `/models`, `/repositories`, `/events`, `/types`,
|
|
11
|
+
`/notifications`, plus the root engine factory). Legacy barrel + service
|
|
12
|
+
layers were removed; hosts wire business flows directly against the
|
|
13
|
+
repositories and the engine factory.
|
|
14
|
+
|
|
15
|
+
### Changed
|
|
16
|
+
|
|
17
|
+
- **Domain restructure.** Dropped the `src/domain/ports/` + `src/services/`
|
|
18
|
+
layer in favor of thin repositories that expose exactly the operations
|
|
19
|
+
hosts need — `earn`, `redeem`, `adjust`, `expire`, referral credit,
|
|
20
|
+
tier recomputation. No more `earning.service` / `redemption.service` /
|
|
21
|
+
`tier.service` / `referral.service` / `ledger.service` / `member.service`
|
|
22
|
+
/ `member-guard` indirections; the equivalent logic lives on the
|
|
23
|
+
repositories where it's naturally atomic.
|
|
24
|
+
- **Events subsystem simplified.** Removed `events/event-bus.ts` +
|
|
25
|
+
`events/event-types.ts` (both are internal indirection that the engine
|
|
26
|
+
didn't need). Event payloads are now colocated with the repository
|
|
27
|
+
that emits them.
|
|
28
|
+
- **Repositories updated to mongokit 3.11 + repo-core 0.2 conventions.**
|
|
29
|
+
Every repo extends `Repository<T>` directly, uses the tenant plugin via
|
|
30
|
+
`injectTenant`, and atomic state changes go through CAS guards.
|
|
31
|
+
- **Peer deps** unchanged in spirit, tightened in range:
|
|
32
|
+
- `@classytic/mongokit >= 3.11`
|
|
33
|
+
- `@classytic/primitives >= 0.1`
|
|
34
|
+
- `@classytic/repo-core >= 0.2`
|
|
35
|
+
- `mongoose >= 9.4`, `zod >= 4`
|
|
36
|
+
- **DevDeps:** `@classytic/primitives` devDep moved from `file:` to `>=0.1.0`
|
|
37
|
+
(now that primitives ships on npm); all `@classytic/*` dev ranges
|
|
38
|
+
standardized on `>=`.
|
|
39
|
+
|
|
40
|
+
### Removed
|
|
41
|
+
|
|
42
|
+
- `src/services/*` — logic folded into repositories.
|
|
43
|
+
- `src/domain/ports/*` barrels and per-port files — structural types
|
|
44
|
+
now live with the repositories they describe.
|
|
45
|
+
- Event-bus indirection — repositories emit via the engine's configured
|
|
46
|
+
transport directly.
|
|
47
|
+
|
|
48
|
+
### Added
|
|
49
|
+
|
|
50
|
+
- Subpath export `/notifications` — notification port + built-in
|
|
51
|
+
lifecycle payloads (tier up/down, points expiring, referral credit).
|
|
52
|
+
- Test restructure — 257 tests across unit + integration projects; the
|
|
53
|
+
deleted legacy suites were collapsed into focused scenario files that
|
|
54
|
+
exercise the new public API.
|
|
55
|
+
|
|
56
|
+
### Peer compatibility
|
|
57
|
+
|
|
58
|
+
Anyone on 0.1.0 wiring against the old `*.service.ts` modules will need
|
|
59
|
+
to migrate to the repository methods. Signatures are similar — earn /
|
|
60
|
+
redeem / adjust remain the nouns — but the call sites move from
|
|
61
|
+
`services.earning.earnPoints(...)` to `repositories.pointTransaction.earn(...)`
|
|
62
|
+
and similar. No behavior changes in the ledger semantics.
|
|
63
|
+
|
|
64
|
+
## [0.1.0]
|
|
65
|
+
|
|
66
|
+
Initial release. Loyalty points, tiers, earning rules, referrals, and
|
|
67
|
+
redemption engine for MongoDB. Tenant-scoped, event-emitting, atomic
|
|
68
|
+
point-ledger with CAS guards.
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { t as NotificationAdapter } from "./notification.port-CJ9nXj0k.mjs";
|
|
2
|
+
import { EventTransport } from "@classytic/primitives/events";
|
|
3
|
+
import { Connection } from "mongoose";
|
|
4
|
+
import { OutboxStore } from "@classytic/primitives/outbox";
|
|
5
|
+
import { TenantConfig, TenantFieldType } from "@classytic/primitives/tenant";
|
|
6
|
+
|
|
7
|
+
//#region src/types/config.d.ts
|
|
8
|
+
type LoyaltyModelName = 'Member' | 'PointTransaction' | 'EarningRule' | 'TierDefinition' | 'Redemption' | 'Referral';
|
|
9
|
+
type TenantConfig$1 = false | TenantConfig;
|
|
10
|
+
interface IndexDefinition {
|
|
11
|
+
fields: Record<string, 1 | -1>;
|
|
12
|
+
options?: Record<string, unknown> | undefined;
|
|
13
|
+
}
|
|
14
|
+
type ModelName = 'member' | 'pointTransaction' | 'earningRule' | 'tierDefinition' | 'redemption' | 'referral';
|
|
15
|
+
type PluginType = unknown;
|
|
16
|
+
interface RepositoryPlugins {
|
|
17
|
+
member?: PluginType[] | undefined;
|
|
18
|
+
pointTransaction?: PluginType[] | undefined;
|
|
19
|
+
earningRule?: PluginType[] | undefined;
|
|
20
|
+
tierDefinition?: PluginType[] | undefined;
|
|
21
|
+
redemption?: PluginType[] | undefined;
|
|
22
|
+
referral?: PluginType[] | undefined;
|
|
23
|
+
}
|
|
24
|
+
interface LoyaltyConfig {
|
|
25
|
+
mongoose: Connection;
|
|
26
|
+
tenant?: TenantConfig$1 | undefined;
|
|
27
|
+
program?: {
|
|
28
|
+
id?: string | undefined;
|
|
29
|
+
conversionRate?: number | undefined;
|
|
30
|
+
} | undefined;
|
|
31
|
+
redemption?: {
|
|
32
|
+
minPoints?: number | undefined;
|
|
33
|
+
minOrderAmount?: number | undefined;
|
|
34
|
+
maxRedeemPercent?: number | undefined;
|
|
35
|
+
reservationTtlMinutes?: number | undefined;
|
|
36
|
+
} | undefined;
|
|
37
|
+
referral?: {
|
|
38
|
+
referrerRewardPoints?: number | undefined;
|
|
39
|
+
refereeRewardPoints?: number | undefined;
|
|
40
|
+
maxPerPeriod?: number | undefined;
|
|
41
|
+
periodDays?: number | undefined;
|
|
42
|
+
requireApproval?: boolean | undefined;
|
|
43
|
+
codeLength?: number | undefined;
|
|
44
|
+
/**
|
|
45
|
+
* Maximum ancestor depth for multi-tier referral rewards. When >= 2,
|
|
46
|
+
* the direct referrer gets `tierRates[0]` percent of
|
|
47
|
+
* `referrerRewardPoints`, the 2nd-tier ancestor gets `tierRates[1]`,
|
|
48
|
+
* etc. Depth 1 = direct referrer (matches single-hop legacy path).
|
|
49
|
+
* Default: 1 (single-hop, preserves prior behaviour).
|
|
50
|
+
*/
|
|
51
|
+
maxDepth?: number | undefined;
|
|
52
|
+
/**
|
|
53
|
+
* Percent rates (0–100) per depth. `tierRates[0]` is the direct
|
|
54
|
+
* referrer's share. Omitted depths fall back to 0. Example:
|
|
55
|
+
* `{ maxDepth: 3, tierRates: [100, 50, 25] }` — direct gets 100%,
|
|
56
|
+
* grand-referrer 50%, great-grand 25%.
|
|
57
|
+
*/
|
|
58
|
+
tierRates?: number[] | undefined;
|
|
59
|
+
} | undefined;
|
|
60
|
+
expiry?: {
|
|
61
|
+
enabled?: boolean | undefined;
|
|
62
|
+
defaultDays?: number | undefined;
|
|
63
|
+
} | undefined;
|
|
64
|
+
indexes?: Partial<Record<ModelName, IndexDefinition[]>> | undefined;
|
|
65
|
+
autoIndex?: boolean | Partial<Record<LoyaltyModelName, boolean>> | undefined;
|
|
66
|
+
/**
|
|
67
|
+
* Optional prefix prepended to every physical collection this package
|
|
68
|
+
* creates (see PACKAGE_RULES.md §20.1). Unset → default names
|
|
69
|
+
* (`loyalty_members`, `loyalty_point_transactions`, `loyalty_earning_rules`,
|
|
70
|
+
* `loyalty_tier_definitions`, `loyalty_redemptions`, `loyalty_referrals`).
|
|
71
|
+
* Model names and `ref:` populate are unaffected.
|
|
72
|
+
*/
|
|
73
|
+
collectionPrefix?: string | undefined;
|
|
74
|
+
/**
|
|
75
|
+
* When true, existing Mongoose models with loyalty's names are deleted
|
|
76
|
+
* from the connection before re-registering. Hot-reload / test fixtures
|
|
77
|
+
* only. Default `false` — collision throws `LoyaltyModelCollisionError`.
|
|
78
|
+
* Hosts that need two loyalty engines should use two Mongoose connections
|
|
79
|
+
* (`mongoose.createConnection(...)`). See PACKAGE_RULES.md §21.
|
|
80
|
+
*/
|
|
81
|
+
forceRecreate?: boolean | undefined;
|
|
82
|
+
/**
|
|
83
|
+
* Arc-compatible event transport. When omitted, the engine uses
|
|
84
|
+
* `InProcessLoyaltyBus` — a ~60-line structural match of arc's
|
|
85
|
+
* `MemoryEventTransport`. See PACKAGE_RULES §13.
|
|
86
|
+
*/
|
|
87
|
+
eventTransport?: EventTransport | undefined;
|
|
88
|
+
/**
|
|
89
|
+
* Host-owned outbox store (PACKAGE_RULES §5.5 + P8). When present, every
|
|
90
|
+
* domain event is persisted via `outbox.save(event, { session })` **before**
|
|
91
|
+
* `eventTransport.publish(event)`. `session` is threaded from `ctx.session`
|
|
92
|
+
* so the outbox row commits atomically with the business write.
|
|
93
|
+
*
|
|
94
|
+
* Loyalty does NOT ship a store. Hosts wire arc's `repositoryAsOutboxStore(mongokitRepo)`
|
|
95
|
+
* over their own `event_outbox` collection, arc's `MemoryOutboxStore`, or any
|
|
96
|
+
* custom Postgres / Redis / Kafka implementation of the contract. When
|
|
97
|
+
* omitted, events flow through `eventTransport` only (no durable log).
|
|
98
|
+
*/
|
|
99
|
+
outbox?: OutboxStore | undefined;
|
|
100
|
+
notifications?: NotificationAdapter | undefined;
|
|
101
|
+
plugins?: RepositoryPlugins | undefined;
|
|
102
|
+
}
|
|
103
|
+
interface ResolvedTenant {
|
|
104
|
+
enabled: boolean;
|
|
105
|
+
tenantField: string;
|
|
106
|
+
ref?: string;
|
|
107
|
+
fieldType: TenantFieldType;
|
|
108
|
+
contextKey: string;
|
|
109
|
+
}
|
|
110
|
+
interface ResolvedConfig {
|
|
111
|
+
programId: string;
|
|
112
|
+
conversionRate: number;
|
|
113
|
+
redemption: {
|
|
114
|
+
minPoints: number;
|
|
115
|
+
minOrderAmount: number;
|
|
116
|
+
maxRedeemPercent: number;
|
|
117
|
+
reservationTtlMinutes: number;
|
|
118
|
+
};
|
|
119
|
+
referral: {
|
|
120
|
+
referrerRewardPoints: number;
|
|
121
|
+
refereeRewardPoints: number;
|
|
122
|
+
maxPerPeriod: number;
|
|
123
|
+
periodDays: number;
|
|
124
|
+
requireApproval: boolean;
|
|
125
|
+
codeLength: number;
|
|
126
|
+
maxDepth: number;
|
|
127
|
+
tierRates: number[];
|
|
128
|
+
};
|
|
129
|
+
expiry: {
|
|
130
|
+
enabled: boolean;
|
|
131
|
+
defaultDays?: number;
|
|
132
|
+
};
|
|
133
|
+
tenant: ResolvedTenant;
|
|
134
|
+
}
|
|
135
|
+
//#endregion
|
|
136
|
+
export { PluginType as a, ResolvedTenant as c, ModelName as i, TenantConfig$1 as l, LoyaltyConfig as n, RepositoryPlugins as o, LoyaltyModelName as r, ResolvedConfig as s, IndexDefinition as t };
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
//#region src/constants.ts
|
|
2
2
|
const DEFAULT_TENANT_FIELD = "organizationId";
|
|
3
|
-
const DEFAULT_TENANT_TYPE = "
|
|
3
|
+
const DEFAULT_TENANT_TYPE = "objectId";
|
|
4
4
|
const DEFAULT_PROGRAM_ID = "default";
|
|
5
5
|
const TRANSACTION_TYPES = Object.freeze([
|
|
6
6
|
"earn",
|
|
@@ -36,7 +36,8 @@ const REDEMPTION_STATUSES = Object.freeze([
|
|
|
36
36
|
"reserved",
|
|
37
37
|
"confirmed",
|
|
38
38
|
"released",
|
|
39
|
-
"expired"
|
|
39
|
+
"expired",
|
|
40
|
+
"reversed"
|
|
40
41
|
]);
|
|
41
42
|
const EARNING_ACTIONS = Object.freeze([
|
|
42
43
|
"signup",
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { a as RuleStatus, c as EARNING_ACTIONS, d as REFERRAL_STATUSES, f as RULE_STATUSES, i as ReferralStatus, l as MEMBER_STATUSES, m as TRANSACTION_TYPES, n as MemberStatus, o as RuleType, p as RULE_TYPES, r as RedemptionStatus, s as TransactionType, t as EarningAction, u as REDEMPTION_STATUSES } from "../../index-
|
|
1
|
+
import { a as RuleStatus, c as EARNING_ACTIONS, d as REFERRAL_STATUSES, f as RULE_STATUSES, i as ReferralStatus, l as MEMBER_STATUSES, m as TRANSACTION_TYPES, n as MemberStatus, o as RuleType, p as RULE_TYPES, r as RedemptionStatus, s as TransactionType, t as EarningAction, u as REDEMPTION_STATUSES } from "../../index-DMK8a98J.mjs";
|
|
2
2
|
export { EARNING_ACTIONS, EarningAction, MEMBER_STATUSES, MemberStatus, REDEMPTION_STATUSES, REFERRAL_STATUSES, RULE_STATUSES, RULE_TYPES, RedemptionStatus, ReferralStatus, RuleStatus, RuleType, TRANSACTION_TYPES, TransactionType };
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { a as MEMBER_STATUSES, c as RULE_STATUSES, i as EARNING_ACTIONS, l as RULE_TYPES, o as REDEMPTION_STATUSES, s as REFERRAL_STATUSES, u as TRANSACTION_TYPES } from "../../constants-
|
|
1
|
+
import { a as MEMBER_STATUSES, c as RULE_STATUSES, i as EARNING_ACTIONS, l as RULE_TYPES, o as REDEMPTION_STATUSES, s as REFERRAL_STATUSES, u as TRANSACTION_TYPES } from "../../constants-DkQYkOK1.mjs";
|
|
2
2
|
export { EARNING_ACTIONS, MEMBER_STATUSES, REDEMPTION_STATUSES, REFERRAL_STATUSES, RULE_STATUSES, RULE_TYPES, TRANSACTION_TYPES };
|
package/dist/events/index.d.mts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { a as
|
|
2
|
-
export {
|
|
1
|
+
import { A as PointsExpired, B as ReferralRewarded, C as MemberDeactivated, D as MultiTierReferralAwarded, E as MemberSuspended, F as RedemptionReserved, G as TierUpgraded, H as TierGraceStarted, I as RedemptionReversed, 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, U as TierOverrideCleared, V as TierDowngraded, W as TierOverrideExpired, Y as createEvent, _ as TierChangedPayload, a as LoyaltyEventMap, b as LoyaltyEventDefinition, c as LoyaltyEvents, d as PointsAdjustedPayload, f as PointsEarnedPayload, g as ReferralPayload, h as RedemptionReversedPayload, i as CampaignAwardedPayload, j as PointsExpiringSoon, k as PointsEarned, l as MemberLifecyclePayload, m as RedemptionPayload, n as EventHandler, o as LoyaltyEventName, p as PointsExpiredPayload, q as InProcessLoyaltyBus, r as EventTransport, s as LoyaltyEventPayload, t as DomainEvent, u as MultiTierReferralAwardedPayload, v as TierOverrideLifecyclePayload, w as MemberEnrolled, x as LoyaltyEventPayloadOf, y as CampaignAwarded, z as ReferralRejected } from "../index-C4jFHS7m.mjs";
|
|
2
|
+
export { CampaignAwarded, CampaignAwardedPayload, DomainEvent, EventHandler, EventTransport, InProcessLoyaltyBus, InProcessLoyaltyBusOptions, LoyaltyEventDefinition, LoyaltyEventMap, LoyaltyEventName, LoyaltyEventPayload, LoyaltyEventPayloadOf, LoyaltyEventSchema, LoyaltyEvents, MemberDeactivated, MemberEnrolled, MemberLifecyclePayload, MemberReactivated, MemberSuspended, MultiTierReferralAwarded, MultiTierReferralAwardedPayload, PointsAdjusted, PointsAdjustedPayload, PointsEarned, PointsEarnedPayload, PointsExpired, PointsExpiredPayload, PointsExpiringSoon, PointsRedeemed, RedemptionConfirmed, RedemptionPayload, RedemptionReleased, RedemptionReserved, RedemptionReversed, RedemptionReversedPayload, ReferralApproved, ReferralPayload, ReferralRecorded, ReferralRejected, ReferralRewarded, TierChangedPayload, TierDowngraded, TierGraceStarted, TierOverrideCleared, TierOverrideExpired, TierOverrideLifecyclePayload, TierUpgraded, createEvent, loyaltyEventDefinitions };
|
package/dist/events/index.mjs
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import { t as
|
|
2
|
-
import { t as LoyaltyEvents } from "../
|
|
3
|
-
export {
|
|
1
|
+
import { C as TierOverrideExpired, E as InProcessLoyaltyBus, S as TierOverrideCleared, T as loyaltyEventDefinitions, _ as ReferralRecorded, a as MemberSuspended, b as TierDowngraded, c as PointsEarned, d as PointsRedeemed, f as RedemptionConfirmed, g as ReferralApproved, h as RedemptionReversed, i as MemberReactivated, l as PointsExpired, m as RedemptionReserved, n as MemberDeactivated, o as MultiTierReferralAwarded, p as RedemptionReleased, r as MemberEnrolled, s as PointsAdjusted, t as CampaignAwarded, u as PointsExpiringSoon, v as ReferralRejected, w as TierUpgraded, x as TierGraceStarted, y as ReferralRewarded } from "../events-BtZOdiPw.mjs";
|
|
2
|
+
import { n as createEvent, t as LoyaltyEvents } from "../loyalty-events-Cyt9TwX6.mjs";
|
|
3
|
+
export { CampaignAwarded, InProcessLoyaltyBus, LoyaltyEvents, MemberDeactivated, MemberEnrolled, MemberReactivated, MemberSuspended, MultiTierReferralAwarded, PointsAdjusted, PointsEarned, PointsExpired, PointsExpiringSoon, PointsRedeemed, RedemptionConfirmed, RedemptionReleased, RedemptionReserved, RedemptionReversed, ReferralApproved, ReferralRecorded, ReferralRejected, ReferralRewarded, TierDowngraded, TierGraceStarted, TierOverrideCleared, TierOverrideExpired, TierUpgraded, createEvent, loyaltyEventDefinitions };
|
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
import { t as LoyaltyEvents } from "./loyalty-events-Cyt9TwX6.mjs";
|
|
2
|
+
import { createEvent, matchEventPattern } from "@classytic/primitives/events";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
//#region src/events/in-process-bus.ts
|
|
5
|
+
var InProcessLoyaltyBus = class {
|
|
6
|
+
name = "in-process-loyalty";
|
|
7
|
+
handlers = /* @__PURE__ */ new Map();
|
|
8
|
+
logger;
|
|
9
|
+
constructor(options) {
|
|
10
|
+
this.logger = options?.logger ?? console;
|
|
11
|
+
}
|
|
12
|
+
async publish(event) {
|
|
13
|
+
const matched = /* @__PURE__ */ new Set();
|
|
14
|
+
for (const [pattern, set] of this.handlers.entries()) if (matchEventPattern(pattern, event.type)) for (const h of set) matched.add(h);
|
|
15
|
+
for (const handler of matched) try {
|
|
16
|
+
await handler(event);
|
|
17
|
+
} catch (err) {
|
|
18
|
+
this.logger.error(`[InProcessLoyaltyBus] Handler error for ${event.type}:`, err);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
async subscribe(pattern, handler) {
|
|
22
|
+
if (!this.handlers.has(pattern)) this.handlers.set(pattern, /* @__PURE__ */ new Set());
|
|
23
|
+
this.handlers.get(pattern)?.add(handler);
|
|
24
|
+
return () => {
|
|
25
|
+
const set = this.handlers.get(pattern);
|
|
26
|
+
if (!set) return;
|
|
27
|
+
set.delete(handler);
|
|
28
|
+
if (set.size === 0) this.handlers.delete(pattern);
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
async close() {
|
|
32
|
+
this.handlers.clear();
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
//#endregion
|
|
36
|
+
//#region src/events/loyalty-event-catalog.ts
|
|
37
|
+
function defineLoyaltyEvent(input) {
|
|
38
|
+
const { name, version = 1, description, zodSchema } = input;
|
|
39
|
+
const def = {
|
|
40
|
+
name,
|
|
41
|
+
version,
|
|
42
|
+
schema: z.toJSONSchema(zodSchema),
|
|
43
|
+
zodSchema,
|
|
44
|
+
create(payload, meta) {
|
|
45
|
+
return createEvent(name, payload, {
|
|
46
|
+
source: "loyalty",
|
|
47
|
+
...meta
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
if (description !== void 0) def.description = description;
|
|
52
|
+
return def;
|
|
53
|
+
}
|
|
54
|
+
/** Mirrors `MemberLifecyclePayload`. */
|
|
55
|
+
const memberLifecycleSchema = z.object({
|
|
56
|
+
memberId: z.string(),
|
|
57
|
+
externalId: z.string(),
|
|
58
|
+
externalType: z.string(),
|
|
59
|
+
status: z.string()
|
|
60
|
+
});
|
|
61
|
+
/** Mirrors `PointsEarnedPayload`. */
|
|
62
|
+
const pointsEarnedSchema = z.object({
|
|
63
|
+
memberId: z.string(),
|
|
64
|
+
externalId: z.string(),
|
|
65
|
+
externalType: z.string(),
|
|
66
|
+
points: z.number(),
|
|
67
|
+
balanceAfter: z.number(),
|
|
68
|
+
transactionId: z.string(),
|
|
69
|
+
earningRuleName: z.string().optional(),
|
|
70
|
+
referenceType: z.string().optional(),
|
|
71
|
+
referenceId: z.string().optional()
|
|
72
|
+
});
|
|
73
|
+
/** Mirrors `PointsAdjustedPayload` — reused by REDEEMED + ADJUSTED. */
|
|
74
|
+
const pointsAdjustedSchema = z.object({
|
|
75
|
+
memberId: z.string(),
|
|
76
|
+
externalId: z.string(),
|
|
77
|
+
points: z.number(),
|
|
78
|
+
balanceAfter: z.number(),
|
|
79
|
+
transactionId: z.string(),
|
|
80
|
+
description: z.string()
|
|
81
|
+
});
|
|
82
|
+
/** Mirrors `PointsExpiredPayload` — reused by EXPIRED + EXPIRING_SOON. */
|
|
83
|
+
const pointsExpiredSchema = z.object({
|
|
84
|
+
membersAffected: z.number().int().nonnegative(),
|
|
85
|
+
pointsExpired: z.number().int().nonnegative(),
|
|
86
|
+
transactionCount: z.number().int().nonnegative()
|
|
87
|
+
});
|
|
88
|
+
/** Mirrors `TierChangedPayload` — reused by UPGRADED + DOWNGRADED + GRACE_STARTED. */
|
|
89
|
+
const tierChangedSchema = z.object({
|
|
90
|
+
memberId: z.string(),
|
|
91
|
+
externalId: z.string(),
|
|
92
|
+
previousTier: z.string().nullable(),
|
|
93
|
+
newTier: z.string().nullable(),
|
|
94
|
+
lifetimePoints: z.number()
|
|
95
|
+
});
|
|
96
|
+
/** Mirrors `RedemptionPayload` — reused by RESERVED + CONFIRMED + RELEASED. */
|
|
97
|
+
const redemptionSchema = z.object({
|
|
98
|
+
memberId: z.string(),
|
|
99
|
+
externalId: z.string(),
|
|
100
|
+
redemptionId: z.string(),
|
|
101
|
+
points: z.number(),
|
|
102
|
+
discountAmount: z.number(),
|
|
103
|
+
ownerType: z.string(),
|
|
104
|
+
ownerId: z.string()
|
|
105
|
+
});
|
|
106
|
+
/** Mirrors `ReferralPayload` — reused across the referral lifecycle. */
|
|
107
|
+
const referralSchema = z.object({
|
|
108
|
+
referralId: z.string(),
|
|
109
|
+
referrerId: z.string(),
|
|
110
|
+
refereeId: z.string(),
|
|
111
|
+
referralCode: z.string(),
|
|
112
|
+
status: z.string(),
|
|
113
|
+
referrerRewardPoints: z.number().optional(),
|
|
114
|
+
refereeRewardPoints: z.number().optional()
|
|
115
|
+
});
|
|
116
|
+
/**
|
|
117
|
+
* Mirrors `TierOverrideLifecyclePayload`. `expiredAt` is serialised as ISO
|
|
118
|
+
* string at transport time (Dates aren't JSON-Schema representable —
|
|
119
|
+
* see zod/v4's `Date cannot be represented in JSON Schema` rule).
|
|
120
|
+
*/
|
|
121
|
+
const tierOverrideLifecycleSchema = z.object({
|
|
122
|
+
memberId: z.string(),
|
|
123
|
+
externalId: z.string(),
|
|
124
|
+
tier: z.string(),
|
|
125
|
+
reason: z.string().optional(),
|
|
126
|
+
expiredAt: z.string().optional()
|
|
127
|
+
});
|
|
128
|
+
/** Mirrors `RedemptionReversedPayload`. */
|
|
129
|
+
const redemptionReversedSchema = z.object({
|
|
130
|
+
memberId: z.string(),
|
|
131
|
+
externalId: z.string(),
|
|
132
|
+
redemptionId: z.string(),
|
|
133
|
+
pointsCredited: z.number(),
|
|
134
|
+
originalPoints: z.number(),
|
|
135
|
+
proportional: z.boolean(),
|
|
136
|
+
reason: z.string().optional(),
|
|
137
|
+
transactionId: z.string()
|
|
138
|
+
});
|
|
139
|
+
/** Mirrors `CampaignAwardedPayload`. */
|
|
140
|
+
const campaignAwardedSchema = z.object({
|
|
141
|
+
campaignId: z.string(),
|
|
142
|
+
memberCount: z.number().int().nonnegative(),
|
|
143
|
+
awarded: z.number().int().nonnegative(),
|
|
144
|
+
skipped: z.number().int().nonnegative(),
|
|
145
|
+
totalPoints: z.number().int().nonnegative()
|
|
146
|
+
});
|
|
147
|
+
/** Mirrors `MultiTierReferralAwardedPayload`. */
|
|
148
|
+
const multiTierReferralAwardedSchema = z.object({
|
|
149
|
+
referralId: z.string(),
|
|
150
|
+
refereeId: z.string(),
|
|
151
|
+
awards: z.array(z.object({
|
|
152
|
+
memberId: z.string(),
|
|
153
|
+
depth: z.number().int().positive(),
|
|
154
|
+
points: z.number().int().nonnegative(),
|
|
155
|
+
transactionId: z.string()
|
|
156
|
+
}))
|
|
157
|
+
});
|
|
158
|
+
const MemberEnrolled = defineLoyaltyEvent({
|
|
159
|
+
name: LoyaltyEvents.MEMBER_ENROLLED,
|
|
160
|
+
description: "A new loyalty member was enrolled in a program.",
|
|
161
|
+
zodSchema: memberLifecycleSchema
|
|
162
|
+
});
|
|
163
|
+
const MemberDeactivated = defineLoyaltyEvent({
|
|
164
|
+
name: LoyaltyEvents.MEMBER_DEACTIVATED,
|
|
165
|
+
description: "A loyalty member was deactivated.",
|
|
166
|
+
zodSchema: memberLifecycleSchema
|
|
167
|
+
});
|
|
168
|
+
const MemberReactivated = defineLoyaltyEvent({
|
|
169
|
+
name: LoyaltyEvents.MEMBER_REACTIVATED,
|
|
170
|
+
description: "A deactivated loyalty member was reactivated.",
|
|
171
|
+
zodSchema: memberLifecycleSchema
|
|
172
|
+
});
|
|
173
|
+
const MemberSuspended = defineLoyaltyEvent({
|
|
174
|
+
name: LoyaltyEvents.MEMBER_SUSPENDED,
|
|
175
|
+
description: "A loyalty member was suspended.",
|
|
176
|
+
zodSchema: memberLifecycleSchema
|
|
177
|
+
});
|
|
178
|
+
const PointsEarned = defineLoyaltyEvent({
|
|
179
|
+
name: LoyaltyEvents.POINTS_EARNED,
|
|
180
|
+
description: "Points were earned by a member through an earning rule.",
|
|
181
|
+
zodSchema: pointsEarnedSchema
|
|
182
|
+
});
|
|
183
|
+
const PointsRedeemed = defineLoyaltyEvent({
|
|
184
|
+
name: LoyaltyEvents.POINTS_REDEEMED,
|
|
185
|
+
description: "Points were redeemed against a discount / reward.",
|
|
186
|
+
zodSchema: pointsAdjustedSchema
|
|
187
|
+
});
|
|
188
|
+
const PointsAdjusted = defineLoyaltyEvent({
|
|
189
|
+
name: LoyaltyEvents.POINTS_ADJUSTED,
|
|
190
|
+
description: "Points were manually adjusted (admin correction / override).",
|
|
191
|
+
zodSchema: pointsAdjustedSchema
|
|
192
|
+
});
|
|
193
|
+
const PointsExpired = defineLoyaltyEvent({
|
|
194
|
+
name: LoyaltyEvents.POINTS_EXPIRED,
|
|
195
|
+
description: "A batch of points expired across one or more members.",
|
|
196
|
+
zodSchema: pointsExpiredSchema
|
|
197
|
+
});
|
|
198
|
+
const PointsExpiringSoon = defineLoyaltyEvent({
|
|
199
|
+
name: LoyaltyEvents.POINTS_EXPIRING_SOON,
|
|
200
|
+
description: "Points are approaching their expiry window — host can notify members.",
|
|
201
|
+
zodSchema: pointsExpiredSchema
|
|
202
|
+
});
|
|
203
|
+
const TierUpgraded = defineLoyaltyEvent({
|
|
204
|
+
name: LoyaltyEvents.TIER_UPGRADED,
|
|
205
|
+
description: "A member qualified for a higher tier.",
|
|
206
|
+
zodSchema: tierChangedSchema
|
|
207
|
+
});
|
|
208
|
+
const TierDowngraded = defineLoyaltyEvent({
|
|
209
|
+
name: LoyaltyEvents.TIER_DOWNGRADED,
|
|
210
|
+
description: "A member dropped to a lower tier after grace expiry.",
|
|
211
|
+
zodSchema: tierChangedSchema
|
|
212
|
+
});
|
|
213
|
+
const TierGraceStarted = defineLoyaltyEvent({
|
|
214
|
+
name: LoyaltyEvents.TIER_GRACE_STARTED,
|
|
215
|
+
description: "A tier grace period was started on a member before potential downgrade.",
|
|
216
|
+
zodSchema: tierChangedSchema
|
|
217
|
+
});
|
|
218
|
+
const TierOverrideCleared = defineLoyaltyEvent({
|
|
219
|
+
name: LoyaltyEvents.TIER_OVERRIDE_CLEARED,
|
|
220
|
+
description: "A manual tier override was explicitly cleared (admin action).",
|
|
221
|
+
zodSchema: tierOverrideLifecycleSchema
|
|
222
|
+
});
|
|
223
|
+
const TierOverrideExpired = defineLoyaltyEvent({
|
|
224
|
+
name: LoyaltyEvents.TIER_OVERRIDE_EXPIRED,
|
|
225
|
+
description: "A tier override with a TTL expired and was swept by cron.",
|
|
226
|
+
zodSchema: tierOverrideLifecycleSchema
|
|
227
|
+
});
|
|
228
|
+
const RedemptionReserved = defineLoyaltyEvent({
|
|
229
|
+
name: LoyaltyEvents.REDEMPTION_RESERVED,
|
|
230
|
+
description: "Points were reserved against a pending redemption.",
|
|
231
|
+
zodSchema: redemptionSchema
|
|
232
|
+
});
|
|
233
|
+
const RedemptionConfirmed = defineLoyaltyEvent({
|
|
234
|
+
name: LoyaltyEvents.REDEMPTION_CONFIRMED,
|
|
235
|
+
description: "A reserved redemption was confirmed — points debited from balance.",
|
|
236
|
+
zodSchema: redemptionSchema
|
|
237
|
+
});
|
|
238
|
+
const RedemptionReleased = defineLoyaltyEvent({
|
|
239
|
+
name: LoyaltyEvents.REDEMPTION_RELEASED,
|
|
240
|
+
description: "A reserved redemption was released — points returned to balance.",
|
|
241
|
+
zodSchema: redemptionSchema
|
|
242
|
+
});
|
|
243
|
+
const RedemptionReversed = defineLoyaltyEvent({
|
|
244
|
+
name: LoyaltyEvents.REDEMPTION_REVERSED,
|
|
245
|
+
description: "A confirmed redemption was reversed — points (fully or partially) credited back.",
|
|
246
|
+
zodSchema: redemptionReversedSchema
|
|
247
|
+
});
|
|
248
|
+
const CampaignAwarded = defineLoyaltyEvent({
|
|
249
|
+
name: LoyaltyEvents.CAMPAIGN_AWARDED,
|
|
250
|
+
description: "A bulk-earn campaign awarded points to a batch of members.",
|
|
251
|
+
zodSchema: campaignAwardedSchema
|
|
252
|
+
});
|
|
253
|
+
const ReferralRecorded = defineLoyaltyEvent({
|
|
254
|
+
name: LoyaltyEvents.REFERRAL_RECORDED,
|
|
255
|
+
description: "A referral was recorded (pending approval when required).",
|
|
256
|
+
zodSchema: referralSchema
|
|
257
|
+
});
|
|
258
|
+
const ReferralApproved = defineLoyaltyEvent({
|
|
259
|
+
name: LoyaltyEvents.REFERRAL_APPROVED,
|
|
260
|
+
description: "A pending referral was approved.",
|
|
261
|
+
zodSchema: referralSchema
|
|
262
|
+
});
|
|
263
|
+
const ReferralRejected = defineLoyaltyEvent({
|
|
264
|
+
name: LoyaltyEvents.REFERRAL_REJECTED,
|
|
265
|
+
description: "A pending referral was rejected.",
|
|
266
|
+
zodSchema: referralSchema
|
|
267
|
+
});
|
|
268
|
+
const ReferralRewarded = defineLoyaltyEvent({
|
|
269
|
+
name: LoyaltyEvents.REFERRAL_REWARDED,
|
|
270
|
+
description: "Referrer and / or referee rewards were awarded for an approved referral.",
|
|
271
|
+
zodSchema: referralSchema
|
|
272
|
+
});
|
|
273
|
+
const MultiTierReferralAwarded = defineLoyaltyEvent({
|
|
274
|
+
name: LoyaltyEvents.MULTI_TIER_REFERRAL_AWARDED,
|
|
275
|
+
description: "Ancestor referrers were awarded points via a multi-tier referral chain.",
|
|
276
|
+
zodSchema: multiTierReferralAwardedSchema
|
|
277
|
+
});
|
|
278
|
+
/**
|
|
279
|
+
* Every loyalty event defined in the package — pass to Arc's
|
|
280
|
+
* `EventRegistry`. Hosts wire ONE array; the whole `loyalty.*` namespace
|
|
281
|
+
* becomes introspectable via OpenAPI and auto-validated at publish time
|
|
282
|
+
* when `eventPlugin({ validateMode: 'reject' })` is set.
|
|
283
|
+
*/
|
|
284
|
+
const loyaltyEventDefinitions = [
|
|
285
|
+
MemberEnrolled,
|
|
286
|
+
MemberDeactivated,
|
|
287
|
+
MemberReactivated,
|
|
288
|
+
MemberSuspended,
|
|
289
|
+
PointsEarned,
|
|
290
|
+
PointsRedeemed,
|
|
291
|
+
PointsAdjusted,
|
|
292
|
+
PointsExpired,
|
|
293
|
+
PointsExpiringSoon,
|
|
294
|
+
TierUpgraded,
|
|
295
|
+
TierDowngraded,
|
|
296
|
+
TierGraceStarted,
|
|
297
|
+
TierOverrideCleared,
|
|
298
|
+
TierOverrideExpired,
|
|
299
|
+
RedemptionReserved,
|
|
300
|
+
RedemptionConfirmed,
|
|
301
|
+
RedemptionReleased,
|
|
302
|
+
RedemptionReversed,
|
|
303
|
+
CampaignAwarded,
|
|
304
|
+
ReferralRecorded,
|
|
305
|
+
ReferralApproved,
|
|
306
|
+
ReferralRejected,
|
|
307
|
+
ReferralRewarded,
|
|
308
|
+
MultiTierReferralAwarded
|
|
309
|
+
];
|
|
310
|
+
//#endregion
|
|
311
|
+
export { TierOverrideExpired as C, InProcessLoyaltyBus as E, TierOverrideCleared as S, loyaltyEventDefinitions as T, ReferralRecorded as _, MemberSuspended as a, TierDowngraded as b, PointsEarned as c, PointsRedeemed as d, RedemptionConfirmed as f, ReferralApproved as g, RedemptionReversed as h, MemberReactivated as i, PointsExpired as l, RedemptionReserved as m, MemberDeactivated as n, MultiTierReferralAwarded as o, RedemptionReleased as p, MemberEnrolled as r, PointsAdjusted as s, CampaignAwarded as t, PointsExpiringSoon as u, ReferralRejected as v, TierUpgraded as w, TierGraceStarted as x, ReferralRewarded as y };
|