@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.
- package/LICENSE +21 -0
- package/README.md +191 -0
- package/dist/config-aNigy4ec.d.mts +90 -0
- package/dist/constants-C75GvizO.mjs +49 -0
- package/dist/domain/enums/index.d.mts +2 -0
- package/dist/domain/enums/index.mjs +2 -0
- package/dist/domain/index.d.mts +90 -0
- package/dist/domain/index.mjs +4 -0
- package/dist/domain-errors-D7q-2LYD.mjs +120 -0
- package/dist/event-emitter.port-CLYPoMni.d.mts +8 -0
- package/dist/event-types-xQBtYBbX.mjs +24 -0
- package/dist/events/index.d.mts +2 -0
- package/dist/events/index.mjs +3 -0
- package/dist/events-CprEWlN7.mjs +25 -0
- package/dist/index-B6ZstxSP.d.mts +2357 -0
- package/dist/index-BcoGeDco.d.mts +127 -0
- package/dist/index-CaH4GhSF.d.mts +19 -0
- package/dist/index-DqzTjeEB.d.mts +148 -0
- package/dist/index-O7TL4R2P.d.mts +86 -0
- package/dist/index-_pg4J11t.d.mts +191 -0
- package/dist/index.d.mts +21 -0
- package/dist/index.mjs +95 -0
- package/dist/models/index.d.mts +2 -0
- package/dist/models/index.mjs +2 -0
- package/dist/models-CrZPmVxt.mjs +417 -0
- package/dist/notification.port-BzUsH0It.d.mts +13 -0
- package/dist/notifications/index.d.mts +21 -0
- package/dist/notifications/index.mjs +19 -0
- package/dist/repositories/index.d.mts +2 -0
- package/dist/repositories/index.mjs +2 -0
- package/dist/repositories-qMX7Xh_y.mjs +440 -0
- package/dist/services/index.d.mts +2 -0
- package/dist/services/index.mjs +2 -0
- package/dist/services-CoTqC4E6.mjs +1098 -0
- package/dist/tier-definition-Djf72UPa.d.mts +120 -0
- package/dist/tier-definition.port-BzrHYBRo.d.mts +143 -0
- package/dist/types/index.d.mts +3 -0
- package/dist/types/index.mjs +1 -0
- 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,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 };
|