@classytic/loyalty 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +191 -0
  3. package/dist/config-aNigy4ec.d.mts +90 -0
  4. package/dist/constants-C75GvizO.mjs +49 -0
  5. package/dist/domain/enums/index.d.mts +2 -0
  6. package/dist/domain/enums/index.mjs +2 -0
  7. package/dist/domain/index.d.mts +90 -0
  8. package/dist/domain/index.mjs +4 -0
  9. package/dist/domain-errors-D7q-2LYD.mjs +120 -0
  10. package/dist/event-emitter.port-CLYPoMni.d.mts +8 -0
  11. package/dist/event-types-xQBtYBbX.mjs +24 -0
  12. package/dist/events/index.d.mts +2 -0
  13. package/dist/events/index.mjs +3 -0
  14. package/dist/events-CprEWlN7.mjs +25 -0
  15. package/dist/index-B6ZstxSP.d.mts +2357 -0
  16. package/dist/index-BcoGeDco.d.mts +127 -0
  17. package/dist/index-CaH4GhSF.d.mts +19 -0
  18. package/dist/index-DqzTjeEB.d.mts +148 -0
  19. package/dist/index-O7TL4R2P.d.mts +86 -0
  20. package/dist/index-_pg4J11t.d.mts +191 -0
  21. package/dist/index.d.mts +21 -0
  22. package/dist/index.mjs +95 -0
  23. package/dist/models/index.d.mts +2 -0
  24. package/dist/models/index.mjs +2 -0
  25. package/dist/models-CrZPmVxt.mjs +417 -0
  26. package/dist/notification.port-BzUsH0It.d.mts +13 -0
  27. package/dist/notifications/index.d.mts +21 -0
  28. package/dist/notifications/index.mjs +19 -0
  29. package/dist/repositories/index.d.mts +2 -0
  30. package/dist/repositories/index.mjs +2 -0
  31. package/dist/repositories-qMX7Xh_y.mjs +440 -0
  32. package/dist/services/index.d.mts +2 -0
  33. package/dist/services/index.mjs +2 -0
  34. package/dist/services-CoTqC4E6.mjs +1098 -0
  35. package/dist/tier-definition-Djf72UPa.d.mts +120 -0
  36. package/dist/tier-definition.port-BzrHYBRo.d.mts +143 -0
  37. package/dist/types/index.d.mts +3 -0
  38. package/dist/types/index.mjs +1 -0
  39. package/package.json +111 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Classytic
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,191 @@
1
+ # @classytic/loyalty
2
+
3
+ Loyalty points, tiers, earning rules, referrals, and redemption engine for MongoDB.
4
+
5
+ Framework-agnostic. Works with Fastify (Arc), Express, NestJS, Next.js, or any Node.js app with a Mongoose connection.
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ npm install @classytic/loyalty
11
+ # Peer deps:
12
+ npm install mongoose@^9 @classytic/mongokit@^3.4
13
+ ```
14
+
15
+ ## Quick Start
16
+
17
+ ```typescript
18
+ import { createLoyaltyEngine } from '@classytic/loyalty';
19
+ import mongoose from 'mongoose';
20
+
21
+ const engine = createLoyaltyEngine({
22
+ mongoose: mongoose.connection,
23
+ tenant: false, // single-tenant (or { field: 'orgId' } for multi-tenant)
24
+ program: { conversionRate: 10 },
25
+ redemption: { minPoints: 100, maxRedeemPercent: 50 },
26
+ referral: { referrerRewardPoints: 200, refereeRewardPoints: 100 },
27
+ });
28
+
29
+ // 6 services ready to use
30
+ const member = await engine.services.member.enroll(
31
+ { externalId: 'cust_1', externalType: 'customer', cardId: 'MBR-001' },
32
+ { actorId: 'admin' },
33
+ );
34
+
35
+ await engine.services.ledger.earnPoints(
36
+ { memberId: member._id, points: 500, description: 'Order #123' },
37
+ { actorId: 'system' },
38
+ );
39
+ ```
40
+
41
+ ## Architecture
42
+
43
+ ```
44
+ createLoyaltyEngine(config)
45
+ -> models 6 Mongoose models (auto-created, OverwriteModel-safe)
46
+ -> repositories 6 repos (tenant-scoped CRUD, pagination, atomic transitions)
47
+ -> services 6 services (the public API)
48
+ -> events In-process event bus (or plug your own)
49
+ ```
50
+
51
+ ### Services
52
+
53
+ | Service | Purpose |
54
+ |---------|---------|
55
+ | `member` | Enroll, deactivate, reactivate, suspend, getByCardId, getByExternalId |
56
+ | `ledger` | Earn points, adjust points, transaction history, process expirations |
57
+ | `earning` | CRUD earning rules (order, action, category, tier_bonus types) |
58
+ | `tier` | CRUD tier definitions, evaluate members, set/clear overrides |
59
+ | `redemption` | Validate, reserve, confirm, release, cleanup expired reservations |
60
+ | `referral` | Generate codes, record referrals, approve/reject, rate limiting |
61
+
62
+ ### Key Design Decisions
63
+
64
+ - **Ledger is source of truth.** `Member.balance` is a denormalized cache updated atomically inside MongoDB transactions.
65
+ - **`cardId` is a first-class field.** Public loyalty card identifier, unique, queryable via `getByCardId()`. Separate from `referralCode`.
66
+ - **Tenant isolation end-to-end.** All reads AND mutations (update, updateBalance, atomicStatusTransition, delete) scope by tenant. No raw `_id` bypass.
67
+ - **Idempotency on both earn and adjust.** Prevents double-crediting on retries, cancel+refund, or network failures.
68
+ - **OverwriteModel-safe.** `createLoyaltyEngine()` can be called multiple times on the same connection (hot reload, tests).
69
+
70
+ ## Using with @classytic/arc
71
+
72
+ The package integrates naturally with Arc's resource pattern. Use `defineResource` with `disableDefaultRoutes: true` and inline handlers that call the engine services:
73
+
74
+ ```typescript
75
+ import { defineResource } from '@classytic/arc';
76
+ import { getLoyaltyEngine } from './loyalty.plugin.js';
77
+
78
+ export const memberResource = defineResource({
79
+ name: 'loyalty-member',
80
+ prefix: '/loyalty/members',
81
+ disableDefaultRoutes: true,
82
+ additionalRoutes: [
83
+ {
84
+ method: 'POST', path: '/', summary: 'Enroll member',
85
+ permissions: permissions.loyalty.manage, wrapHandler: false,
86
+ schema: memberSchemas.enroll,
87
+ handler: async (req, reply) => {
88
+ const member = await getLoyaltyEngine().services.member.enroll(
89
+ { externalId: req.body.customerId, externalType: 'customer' },
90
+ { actorId: req.user.id },
91
+ );
92
+ return reply.code(201).send({ success: true, data: member });
93
+ },
94
+ },
95
+ // ... more routes
96
+ ],
97
+ });
98
+ ```
99
+
100
+ This is the same pattern used for warehouse resources (`warehouse.resources.ts`) in Arc-based projects.
101
+
102
+ ### Compatibility
103
+
104
+ | Concept | Arc | Loyalty | Status |
105
+ |---------|-----|---------|--------|
106
+ | `PaginatedResult<T>` | `{ docs, page, limit, total, pages, hasNext, hasPrev }` | Same shape | Identical |
107
+ | Repository | `RepositoryLike` / `CrudRepository<T>` | Domain ports (MemberPort, etc.) | Compatible |
108
+ | Transactions | `session?: unknown` in QueryOptions | `session?: TransactionSession` in method params | Compatible |
109
+ | Events | Arc event registry | `engine.events` (EventEmitterPort) | Bridgeable |
110
+
111
+ ## Multi-tenant
112
+
113
+ ```typescript
114
+ // Single company, multi-branch (Nike with stores)
115
+ createLoyaltyEngine({ tenant: false });
116
+ // Store branchId in transaction metadata for analytics
117
+
118
+ // Multi-tenant SaaS (Shopify-like)
119
+ createLoyaltyEngine({ tenant: { field: 'organizationId' } });
120
+
121
+ // Custom tenant field
122
+ createLoyaltyEngine({ tenant: { field: 'companyId', type: 'string', contextKey: 'companyId' } });
123
+ ```
124
+
125
+ ## Typed DTOs
126
+
127
+ All public service methods accept typed inputs:
128
+
129
+ ```typescript
130
+ import type {
131
+ EnrollInput,
132
+ EarnPointsInput,
133
+ AdjustPointsInput,
134
+ CreateEarningRuleInput,
135
+ UpdateEarningRuleInput,
136
+ CreateTierInput,
137
+ UpdateTierInput,
138
+ LoyaltyContext,
139
+ } from '@classytic/loyalty';
140
+ ```
141
+
142
+ ## Events
143
+
144
+ ```typescript
145
+ import { LoyaltyEvents } from '@classytic/loyalty/events';
146
+
147
+ engine.events.on(LoyaltyEvents.POINTS_EARNED, (payload) => { /* sync projection */ });
148
+ engine.events.on(LoyaltyEvents.TIER_UPGRADED, (payload) => { /* notify customer */ });
149
+ engine.events.on(LoyaltyEvents.REFERRAL_REWARDED, (payload) => { /* notify both */ });
150
+ ```
151
+
152
+ ## Production Cron Jobs
153
+
154
+ ```typescript
155
+ // Every 5 min — release expired point reservations
156
+ await engine.services.redemption.cleanupExpired({ actorId: 'cron' });
157
+
158
+ // Every hour — expire points past their expiresAt
159
+ await engine.services.ledger.processExpirations({ actorId: 'cron' });
160
+
161
+ // Every 24h — re-evaluate all member tiers
162
+ await engine.services.tier.evaluateAll({ actorId: 'cron' });
163
+ ```
164
+
165
+ ## Error Codes
166
+
167
+ | Code | HTTP | When |
168
+ |------|------|------|
169
+ | `MEMBER_NOT_FOUND` | 404 | Member doesn't exist |
170
+ | `MEMBER_ALREADY_ENROLLED` | 409 | Duplicate enrollment |
171
+ | `MEMBER_INACTIVE` | 422 | Operating on inactive member |
172
+ | `INSUFFICIENT_POINTS` | 400 | Not enough points to deduct |
173
+ | `RULE_NOT_FOUND` | 404 | Earning rule doesn't exist |
174
+ | `TIER_NOT_FOUND` | 404 | Tier definition doesn't exist |
175
+ | `REDEMPTION_EXPIRED` | 410 | Reservation TTL exceeded |
176
+ | `DUPLICATE_REFERRAL` | 409 | Referee already referred |
177
+ | `SELF_REFERRAL` | 422 | Referring yourself |
178
+ | `REFERRAL_LIMIT_EXCEEDED` | 429 | Max referrals per period |
179
+ | `TENANT_ISOLATION` | 403 | Cross-tenant access attempt |
180
+
181
+ ## AI Agent Skill
182
+
183
+ Install the agent skill for AI-assisted integration:
184
+
185
+ ```bash
186
+ npx skills add classytic/loyalty
187
+ ```
188
+
189
+ ## License
190
+
191
+ MIT
@@ -0,0 +1,90 @@
1
+ import { t as EventEmitterPort } from "./event-emitter.port-CLYPoMni.mjs";
2
+ import { t as NotificationAdapter } from "./notification.port-BzUsH0It.mjs";
3
+ import { Connection } from "mongoose";
4
+
5
+ //#region src/types/config.d.ts
6
+ interface TenantEnabled {
7
+ field?: string;
8
+ ref?: string;
9
+ type?: 'ObjectId' | 'string';
10
+ contextKey?: string;
11
+ }
12
+ type TenantConfig = false | TenantEnabled;
13
+ interface IndexDefinition {
14
+ fields: Record<string, 1 | -1>;
15
+ options?: Record<string, unknown>;
16
+ }
17
+ type ModelName = 'member' | 'pointTransaction' | 'earningRule' | 'tierDefinition' | 'redemption' | 'referral';
18
+ type PluginType = unknown;
19
+ interface RepositoryPlugins {
20
+ member?: PluginType[];
21
+ pointTransaction?: PluginType[];
22
+ earningRule?: PluginType[];
23
+ tierDefinition?: PluginType[];
24
+ redemption?: PluginType[];
25
+ referral?: PluginType[];
26
+ }
27
+ interface LoyaltyConfig {
28
+ mongoose: Connection;
29
+ tenant?: TenantConfig;
30
+ program?: {
31
+ id?: string;
32
+ conversionRate?: number;
33
+ };
34
+ redemption?: {
35
+ minPoints?: number;
36
+ minOrderAmount?: number;
37
+ maxRedeemPercent?: number;
38
+ reservationTtlMinutes?: number;
39
+ };
40
+ referral?: {
41
+ referrerRewardPoints?: number;
42
+ refereeRewardPoints?: number;
43
+ maxPerPeriod?: number;
44
+ periodDays?: number;
45
+ requireApproval?: boolean;
46
+ codeLength?: number;
47
+ };
48
+ expiry?: {
49
+ enabled?: boolean;
50
+ defaultDays?: number;
51
+ };
52
+ indexes?: Partial<Record<ModelName, IndexDefinition[]>>;
53
+ events?: {
54
+ adapter?: EventEmitterPort;
55
+ };
56
+ notifications?: NotificationAdapter;
57
+ plugins?: RepositoryPlugins;
58
+ }
59
+ interface ResolvedTenant {
60
+ enabled: boolean;
61
+ field: string;
62
+ ref?: string;
63
+ type: 'ObjectId' | 'string';
64
+ contextKey: string;
65
+ }
66
+ interface ResolvedConfig {
67
+ programId: string;
68
+ conversionRate: number;
69
+ redemption: {
70
+ minPoints: number;
71
+ minOrderAmount: number;
72
+ maxRedeemPercent: number;
73
+ reservationTtlMinutes: number;
74
+ };
75
+ referral: {
76
+ referrerRewardPoints: number;
77
+ refereeRewardPoints: number;
78
+ maxPerPeriod: number;
79
+ periodDays: number;
80
+ requireApproval: boolean;
81
+ codeLength: number;
82
+ };
83
+ expiry: {
84
+ enabled: boolean;
85
+ defaultDays?: number;
86
+ };
87
+ tenant: ResolvedTenant;
88
+ }
89
+ //#endregion
90
+ export { RepositoryPlugins as a, TenantConfig as c, PluginType as i, LoyaltyConfig as n, ResolvedConfig as o, ModelName as r, ResolvedTenant as s, IndexDefinition as t };
@@ -0,0 +1,49 @@
1
+ //#region src/constants.ts
2
+ const DEFAULT_TENANT_FIELD = "organizationId";
3
+ const DEFAULT_TENANT_TYPE = "ObjectId";
4
+ const DEFAULT_PROGRAM_ID = "default";
5
+ const TRANSACTION_TYPES = Object.freeze([
6
+ "earn",
7
+ "redeem",
8
+ "adjust",
9
+ "expire",
10
+ "transfer",
11
+ "referral_bonus"
12
+ ]);
13
+ const MEMBER_STATUSES = Object.freeze([
14
+ "active",
15
+ "inactive",
16
+ "suspended"
17
+ ]);
18
+ const RULE_TYPES = Object.freeze([
19
+ "order",
20
+ "action",
21
+ "category",
22
+ "tier_bonus"
23
+ ]);
24
+ const RULE_STATUSES = Object.freeze([
25
+ "active",
26
+ "paused",
27
+ "expired"
28
+ ]);
29
+ const REFERRAL_STATUSES = Object.freeze([
30
+ "pending",
31
+ "approved",
32
+ "rejected",
33
+ "expired"
34
+ ]);
35
+ const REDEMPTION_STATUSES = Object.freeze([
36
+ "reserved",
37
+ "confirmed",
38
+ "released",
39
+ "expired"
40
+ ]);
41
+ const EARNING_ACTIONS = Object.freeze([
42
+ "signup",
43
+ "referral",
44
+ "birthday",
45
+ "review",
46
+ "profile_complete"
47
+ ]);
48
+ //#endregion
49
+ export { MEMBER_STATUSES as a, RULE_STATUSES as c, EARNING_ACTIONS as i, RULE_TYPES as l, DEFAULT_TENANT_FIELD as n, REDEMPTION_STATUSES as o, DEFAULT_TENANT_TYPE as r, REFERRAL_STATUSES as s, DEFAULT_PROGRAM_ID as t, TRANSACTION_TYPES as u };
@@ -0,0 +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-CaH4GhSF.mjs";
2
+ export { EARNING_ACTIONS, EarningAction, MEMBER_STATUSES, MemberStatus, REDEMPTION_STATUSES, REFERRAL_STATUSES, RULE_STATUSES, RULE_TYPES, RedemptionStatus, ReferralStatus, RuleStatus, RuleType, TRANSACTION_TYPES, TransactionType };
@@ -0,0 +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-C75GvizO.mjs";
2
+ export { EARNING_ACTIONS, MEMBER_STATUSES, REDEMPTION_STATUSES, REFERRAL_STATUSES, RULE_STATUSES, RULE_TYPES, TRANSACTION_TYPES };
@@ -0,0 +1,90 @@
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-CaH4GhSF.mjs";
2
+ import { a as PointTransaction, c as EarningConditions, i as TierQualification, l as EarningReward, n as TierDefinition, o as Member, r as TierDowngrade, s as MemberBalance, t as TierBenefits, u as EarningRule } from "../tier-definition-Djf72UPa.mjs";
3
+ import { a as MemberPort, c as UnitOfWork, i as PointTransactionPort, l as Referral, n as ReferralPort, o as EarningRulePort, r as RedemptionPort, s as TransactionSession, t as TierDefinitionPort, u as Redemption } from "../tier-definition.port-BzrHYBRo.mjs";
4
+ import { t as EventEmitterPort } from "../event-emitter.port-CLYPoMni.mjs";
5
+ import { t as NotificationAdapter } from "../notification.port-BzUsH0It.mjs";
6
+
7
+ //#region src/domain/errors/base.d.ts
8
+ declare abstract class LoyaltyError extends Error {
9
+ abstract readonly code: string;
10
+ abstract readonly httpStatus: number;
11
+ constructor(message: string);
12
+ toJSON(): {
13
+ code: string;
14
+ message: string;
15
+ name: string;
16
+ };
17
+ }
18
+ //#endregion
19
+ //#region src/domain/errors/domain-errors.d.ts
20
+ declare class InsufficientPointsError extends LoyaltyError {
21
+ readonly code = "INSUFFICIENT_POINTS";
22
+ readonly httpStatus = 400;
23
+ constructor(available: number, requested: number);
24
+ }
25
+ declare class MemberNotFoundError extends LoyaltyError {
26
+ readonly code = "MEMBER_NOT_FOUND";
27
+ readonly httpStatus = 404;
28
+ constructor(id: string);
29
+ }
30
+ declare class MemberAlreadyEnrolledError extends LoyaltyError {
31
+ readonly code = "MEMBER_ALREADY_ENROLLED";
32
+ readonly httpStatus = 409;
33
+ constructor(externalId: string);
34
+ }
35
+ declare class MemberInactiveError extends LoyaltyError {
36
+ readonly code = "MEMBER_INACTIVE";
37
+ readonly httpStatus = 422;
38
+ constructor(id: string);
39
+ }
40
+ declare class RuleNotFoundError extends LoyaltyError {
41
+ readonly code = "RULE_NOT_FOUND";
42
+ readonly httpStatus = 404;
43
+ constructor(id: string);
44
+ }
45
+ declare class TierNotFoundError extends LoyaltyError {
46
+ readonly code = "TIER_NOT_FOUND";
47
+ readonly httpStatus = 404;
48
+ constructor(id: string);
49
+ }
50
+ declare class RedemptionNotFoundError extends LoyaltyError {
51
+ readonly code = "REDEMPTION_NOT_FOUND";
52
+ readonly httpStatus = 404;
53
+ constructor(id: string);
54
+ }
55
+ declare class RedemptionExpiredError extends LoyaltyError {
56
+ readonly code = "REDEMPTION_EXPIRED";
57
+ readonly httpStatus = 410;
58
+ constructor(id: string);
59
+ }
60
+ declare class ReferralNotFoundError extends LoyaltyError {
61
+ readonly code = "REFERRAL_NOT_FOUND";
62
+ readonly httpStatus = 404;
63
+ constructor(id: string);
64
+ }
65
+ declare class DuplicateReferralError extends LoyaltyError {
66
+ readonly code = "DUPLICATE_REFERRAL";
67
+ readonly httpStatus = 409;
68
+ constructor(refereeId: string);
69
+ }
70
+ declare class ReferralLimitExceededError extends LoyaltyError {
71
+ readonly code = "REFERRAL_LIMIT_EXCEEDED";
72
+ readonly httpStatus = 429;
73
+ constructor(limit: number, periodDays: number);
74
+ }
75
+ declare class SelfReferralError extends LoyaltyError {
76
+ readonly code = "SELF_REFERRAL";
77
+ readonly httpStatus = 422;
78
+ constructor();
79
+ }
80
+ declare class ValidationError extends LoyaltyError {
81
+ readonly code = "VALIDATION_ERROR";
82
+ readonly httpStatus = 400;
83
+ }
84
+ declare class TenantIsolationError extends LoyaltyError {
85
+ readonly code = "TENANT_ISOLATION";
86
+ readonly httpStatus = 403;
87
+ constructor();
88
+ }
89
+ //#endregion
90
+ export { DuplicateReferralError, EARNING_ACTIONS, EarningAction, EarningConditions, EarningReward, EarningRule, EarningRulePort, EventEmitterPort, InsufficientPointsError, LoyaltyError, MEMBER_STATUSES, Member, MemberAlreadyEnrolledError, MemberBalance, MemberInactiveError, MemberNotFoundError, MemberPort, MemberStatus, NotificationAdapter, PointTransaction, PointTransactionPort, REDEMPTION_STATUSES, REFERRAL_STATUSES, RULE_STATUSES, RULE_TYPES, Redemption, RedemptionExpiredError, RedemptionNotFoundError, RedemptionPort, RedemptionStatus, Referral, ReferralLimitExceededError, ReferralNotFoundError, ReferralPort, ReferralStatus, RuleNotFoundError, RuleStatus, RuleType, SelfReferralError, TRANSACTION_TYPES, TenantIsolationError, TierBenefits, TierDefinition, TierDefinitionPort, TierDowngrade, TierNotFoundError, TierQualification, TransactionSession, TransactionType, UnitOfWork, ValidationError };
@@ -0,0 +1,4 @@
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-C75GvizO.mjs";
2
+ import { a as MemberInactiveError, c as RedemptionNotFoundError, d as RuleNotFoundError, f as SelfReferralError, g as LoyaltyError, h as ValidationError, i as MemberAlreadyEnrolledError, l as ReferralLimitExceededError, m as TierNotFoundError, n as DuplicateReferralError, o as MemberNotFoundError, p as TenantIsolationError, r as InsufficientPointsError, s as RedemptionExpiredError, u as ReferralNotFoundError } from "../domain-errors-D7q-2LYD.mjs";
3
+ import "./enums/index.mjs";
4
+ export { DuplicateReferralError, EARNING_ACTIONS, InsufficientPointsError, LoyaltyError, MEMBER_STATUSES, MemberAlreadyEnrolledError, MemberInactiveError, MemberNotFoundError, REDEMPTION_STATUSES, REFERRAL_STATUSES, RULE_STATUSES, RULE_TYPES, RedemptionExpiredError, RedemptionNotFoundError, ReferralLimitExceededError, ReferralNotFoundError, RuleNotFoundError, SelfReferralError, TRANSACTION_TYPES, TenantIsolationError, TierNotFoundError, ValidationError };
@@ -0,0 +1,120 @@
1
+ //#region src/domain/errors/base.ts
2
+ var LoyaltyError = class extends Error {
3
+ constructor(message) {
4
+ super(message);
5
+ this.name = this.constructor.name;
6
+ }
7
+ toJSON() {
8
+ return {
9
+ code: this.code,
10
+ message: this.message,
11
+ name: this.name
12
+ };
13
+ }
14
+ };
15
+ //#endregion
16
+ //#region src/domain/errors/domain-errors.ts
17
+ var InsufficientPointsError = class extends LoyaltyError {
18
+ code = "INSUFFICIENT_POINTS";
19
+ httpStatus = 400;
20
+ constructor(available, requested) {
21
+ super(`Insufficient points. Available: ${available}, requested: ${requested}`);
22
+ }
23
+ };
24
+ var MemberNotFoundError = class extends LoyaltyError {
25
+ code = "MEMBER_NOT_FOUND";
26
+ httpStatus = 404;
27
+ constructor(id) {
28
+ super(`Member not found: ${id}`);
29
+ }
30
+ };
31
+ var MemberAlreadyEnrolledError = class extends LoyaltyError {
32
+ code = "MEMBER_ALREADY_ENROLLED";
33
+ httpStatus = 409;
34
+ constructor(externalId) {
35
+ super(`Member already enrolled: ${externalId}`);
36
+ }
37
+ };
38
+ var MemberInactiveError = class extends LoyaltyError {
39
+ code = "MEMBER_INACTIVE";
40
+ httpStatus = 422;
41
+ constructor(id) {
42
+ super(`Member is not active: ${id}`);
43
+ }
44
+ };
45
+ var RuleNotFoundError = class extends LoyaltyError {
46
+ code = "RULE_NOT_FOUND";
47
+ httpStatus = 404;
48
+ constructor(id) {
49
+ super(`Earning rule not found: ${id}`);
50
+ }
51
+ };
52
+ var TierNotFoundError = class extends LoyaltyError {
53
+ code = "TIER_NOT_FOUND";
54
+ httpStatus = 404;
55
+ constructor(id) {
56
+ super(`Tier not found: ${id}`);
57
+ }
58
+ };
59
+ var RedemptionNotFoundError = class extends LoyaltyError {
60
+ code = "REDEMPTION_NOT_FOUND";
61
+ httpStatus = 404;
62
+ constructor(id) {
63
+ super(`Redemption not found: ${id}`);
64
+ }
65
+ };
66
+ var RedemptionExpiredError = class extends LoyaltyError {
67
+ code = "REDEMPTION_EXPIRED";
68
+ httpStatus = 410;
69
+ constructor(id) {
70
+ super(`Redemption expired: ${id}`);
71
+ }
72
+ };
73
+ var ReferralNotFoundError = class extends LoyaltyError {
74
+ code = "REFERRAL_NOT_FOUND";
75
+ httpStatus = 404;
76
+ constructor(id) {
77
+ super(`Referral not found: ${id}`);
78
+ }
79
+ };
80
+ var DuplicateReferralError = class extends LoyaltyError {
81
+ code = "DUPLICATE_REFERRAL";
82
+ httpStatus = 409;
83
+ constructor(refereeId) {
84
+ super(`Referee already has a referral: ${refereeId}`);
85
+ }
86
+ };
87
+ var ReferralLimitExceededError = class extends LoyaltyError {
88
+ code = "REFERRAL_LIMIT_EXCEEDED";
89
+ httpStatus = 429;
90
+ constructor(limit, periodDays) {
91
+ super(`Referral limit of ${limit} per ${periodDays} days exceeded`);
92
+ }
93
+ };
94
+ var SelfReferralError = class extends LoyaltyError {
95
+ code = "SELF_REFERRAL";
96
+ httpStatus = 422;
97
+ constructor() {
98
+ super("Cannot refer yourself");
99
+ }
100
+ };
101
+ var CircularReferralError = class extends LoyaltyError {
102
+ code = "CIRCULAR_REFERRAL";
103
+ httpStatus = 422;
104
+ constructor() {
105
+ super("Circular referral detected");
106
+ }
107
+ };
108
+ var ValidationError = class extends LoyaltyError {
109
+ code = "VALIDATION_ERROR";
110
+ httpStatus = 400;
111
+ };
112
+ var TenantIsolationError = class extends LoyaltyError {
113
+ code = "TENANT_ISOLATION";
114
+ httpStatus = 403;
115
+ constructor() {
116
+ super("Tenant context is required but not provided");
117
+ }
118
+ };
119
+ //#endregion
120
+ export { MemberInactiveError as a, RedemptionNotFoundError as c, RuleNotFoundError as d, SelfReferralError as f, LoyaltyError as g, ValidationError as h, MemberAlreadyEnrolledError as i, ReferralLimitExceededError as l, TierNotFoundError as m, DuplicateReferralError as n, MemberNotFoundError as o, TenantIsolationError as p, InsufficientPointsError as r, RedemptionExpiredError as s, CircularReferralError as t, ReferralNotFoundError as u };
@@ -0,0 +1,8 @@
1
+ //#region src/domain/ports/event-emitter.port.d.ts
2
+ interface EventEmitterPort {
3
+ emit(event: string, payload: unknown): void;
4
+ on(event: string, handler: (payload: unknown) => void | Promise<void>): void;
5
+ off(event: string, handler: (payload: unknown) => void | Promise<void>): void;
6
+ }
7
+ //#endregion
8
+ export { EventEmitterPort as t };
@@ -0,0 +1,24 @@
1
+ //#region src/events/event-types.ts
2
+ const LoyaltyEvents = {
3
+ MEMBER_ENROLLED: "loyalty.member.enrolled",
4
+ MEMBER_DEACTIVATED: "loyalty.member.deactivated",
5
+ MEMBER_REACTIVATED: "loyalty.member.reactivated",
6
+ MEMBER_SUSPENDED: "loyalty.member.suspended",
7
+ POINTS_EARNED: "loyalty.points.earned",
8
+ POINTS_REDEEMED: "loyalty.points.redeemed",
9
+ POINTS_ADJUSTED: "loyalty.points.adjusted",
10
+ POINTS_EXPIRED: "loyalty.points.expired",
11
+ POINTS_EXPIRING_SOON: "loyalty.points.expiring_soon",
12
+ TIER_UPGRADED: "loyalty.tier.upgraded",
13
+ TIER_DOWNGRADED: "loyalty.tier.downgraded",
14
+ TIER_GRACE_STARTED: "loyalty.tier.grace_started",
15
+ REDEMPTION_RESERVED: "loyalty.redemption.reserved",
16
+ REDEMPTION_CONFIRMED: "loyalty.redemption.confirmed",
17
+ REDEMPTION_RELEASED: "loyalty.redemption.released",
18
+ REFERRAL_RECORDED: "loyalty.referral.recorded",
19
+ REFERRAL_APPROVED: "loyalty.referral.approved",
20
+ REFERRAL_REJECTED: "loyalty.referral.rejected",
21
+ REFERRAL_REWARDED: "loyalty.referral.rewarded"
22
+ };
23
+ //#endregion
24
+ export { LoyaltyEvents as t };
@@ -0,0 +1,2 @@
1
+ import { a as PointsEarnedPayload, c as TierChangedPayload, i as PointsAdjustedPayload, l as InProcessEventBus, n as LoyaltyEvents, o as RedemptionPayload, r as MemberLifecyclePayload, s as ReferralPayload, t as LoyaltyEvent } from "../index-O7TL4R2P.mjs";
2
+ export { InProcessEventBus, LoyaltyEvent, LoyaltyEvents, MemberLifecyclePayload, PointsAdjustedPayload, PointsEarnedPayload, RedemptionPayload, ReferralPayload, TierChangedPayload };
@@ -0,0 +1,3 @@
1
+ import { t as InProcessEventBus } from "../events-CprEWlN7.mjs";
2
+ import { t as LoyaltyEvents } from "../event-types-xQBtYBbX.mjs";
3
+ export { InProcessEventBus, LoyaltyEvents };
@@ -0,0 +1,25 @@
1
+ //#region src/events/event-bus.ts
2
+ var InProcessEventBus = class {
3
+ handlers = /* @__PURE__ */ new Map();
4
+ emit(event, payload) {
5
+ const set = this.handlers.get(event);
6
+ if (!set) return;
7
+ for (const handler of set) try {
8
+ const result = handler(payload);
9
+ if (result && typeof result.catch === "function") result.catch(() => {});
10
+ } catch {}
11
+ }
12
+ on(event, handler) {
13
+ let set = this.handlers.get(event);
14
+ if (!set) {
15
+ set = /* @__PURE__ */ new Set();
16
+ this.handlers.set(event, set);
17
+ }
18
+ set.add(handler);
19
+ }
20
+ off(event, handler) {
21
+ this.handlers.get(event)?.delete(handler);
22
+ }
23
+ };
24
+ //#endregion
25
+ export { InProcessEventBus as t };