@soulbatical/tetra-core 0.1.7 → 0.1.9
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.
Potentially problematic release.
This version of @soulbatical/tetra-core might be problematic. Click here for more details.
- package/dist/index.d.ts +9 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +9 -0
- package/dist/index.js.map +1 -1
- package/dist/middleware/entitlementsMiddleware.d.ts +142 -0
- package/dist/middleware/entitlementsMiddleware.d.ts.map +1 -0
- package/dist/middleware/entitlementsMiddleware.js +246 -0
- package/dist/middleware/entitlementsMiddleware.js.map +1 -0
- package/dist/middleware/permissionsMiddleware.d.ts +181 -0
- package/dist/middleware/permissionsMiddleware.d.ts.map +1 -0
- package/dist/middleware/permissionsMiddleware.js +237 -0
- package/dist/middleware/permissionsMiddleware.js.map +1 -0
- package/dist/shared/affiliate/AffiliateAttributionService.d.ts +47 -0
- package/dist/shared/affiliate/AffiliateAttributionService.d.ts.map +1 -0
- package/dist/shared/affiliate/AffiliateAttributionService.js +308 -0
- package/dist/shared/affiliate/AffiliateAttributionService.js.map +1 -0
- package/dist/shared/affiliate/AffiliateClickService.d.ts +35 -0
- package/dist/shared/affiliate/AffiliateClickService.d.ts.map +1 -0
- package/dist/shared/affiliate/AffiliateClickService.js +87 -0
- package/dist/shared/affiliate/AffiliateClickService.js.map +1 -0
- package/dist/shared/affiliate/affiliateFeatureConfig.d.ts +11 -0
- package/dist/shared/affiliate/affiliateFeatureConfig.d.ts.map +1 -0
- package/dist/shared/affiliate/affiliateFeatureConfig.js +242 -0
- package/dist/shared/affiliate/affiliateFeatureConfig.js.map +1 -0
- package/dist/shared/affiliate/index.d.ts +11 -0
- package/dist/shared/affiliate/index.d.ts.map +1 -0
- package/dist/shared/affiliate/index.js +13 -0
- package/dist/shared/affiliate/index.js.map +1 -0
- package/dist/shared/affiliate/routes.d.ts +87 -0
- package/dist/shared/affiliate/routes.d.ts.map +1 -0
- package/dist/shared/affiliate/routes.js +404 -0
- package/dist/shared/affiliate/routes.js.map +1 -0
- package/dist/shared/affiliate/types.d.ts +170 -0
- package/dist/shared/affiliate/types.d.ts.map +1 -0
- package/dist/shared/affiliate/types.js +11 -0
- package/dist/shared/affiliate/types.js.map +1 -0
- package/package.json +1 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"permissionsMiddleware.d.ts","sourceRoot":"","sources":["../../src/middleware/permissionsMiddleware.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @tetra/core — Permissions Middleware (Config-Driven RBAC)
|
|
3
|
+
*
|
|
4
|
+
* Controls what a USER can do based on their role within a feature.
|
|
5
|
+
* This is separate from entitlements (what an ORG can do based on their plan).
|
|
6
|
+
*
|
|
7
|
+
* Projects call configurePermissions() once at startup with their feature
|
|
8
|
+
* permission configs. Each feature defines which roles can create/read/update/delete.
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* ```typescript
|
|
12
|
+
* configurePermissions({
|
|
13
|
+
* notes: {
|
|
14
|
+
* resource: 'notes',
|
|
15
|
+
* roles: {
|
|
16
|
+
* admin: { create: true, read: true, update: true, delete: true },
|
|
17
|
+
* coach: { create: true, read: true, update: 'own', delete: 'own' },
|
|
18
|
+
* client: { create: false, read: true, update: false, delete: false },
|
|
19
|
+
* },
|
|
20
|
+
* ui: {
|
|
21
|
+
* nav: ['admin', 'coach'],
|
|
22
|
+
* tabs: { notities: ['admin', 'coach'] },
|
|
23
|
+
* actions: { create_note: ['admin', 'coach'] },
|
|
24
|
+
* },
|
|
25
|
+
* },
|
|
26
|
+
* });
|
|
27
|
+
*
|
|
28
|
+
* // In routes:
|
|
29
|
+
* router.post('/', requirePermission('notes', 'create'), handler);
|
|
30
|
+
* router.delete('/:id', requirePermission('notes', 'delete'), handler);
|
|
31
|
+
* ```
|
|
32
|
+
*/
|
|
33
|
+
import { createLogger } from '../utils/logger.js';
|
|
34
|
+
import { RFC7807ErrorResponse } from '../shared/rfc7807ErrorResponse.js';
|
|
35
|
+
const logger = createLogger('middleware:permissions');
|
|
36
|
+
// ─── State ──────────────────────────────────────────────────
|
|
37
|
+
const permissionConfigs = new Map();
|
|
38
|
+
// ─── Configure ──────────────────────────────────────────────
|
|
39
|
+
/**
|
|
40
|
+
* Register permission configs for features. Call once at app startup.
|
|
41
|
+
* Can be called multiple times to add more features.
|
|
42
|
+
*
|
|
43
|
+
* @param configs - Map of resource name → FeaturePermissions
|
|
44
|
+
*
|
|
45
|
+
* @example
|
|
46
|
+
* configurePermissions({
|
|
47
|
+
* notes: notesPermissions,
|
|
48
|
+
* tracks: tracksPermissions,
|
|
49
|
+
* });
|
|
50
|
+
*/
|
|
51
|
+
export function configurePermissions(configs) {
|
|
52
|
+
for (const [key, config] of Object.entries(configs)) {
|
|
53
|
+
permissionConfigs.set(key, config);
|
|
54
|
+
}
|
|
55
|
+
logger.info({ features: Object.keys(configs) }, `Permissions configured for ${Object.keys(configs).length} feature(s)`);
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Register a single feature's permissions.
|
|
59
|
+
*/
|
|
60
|
+
export function registerFeaturePermissions(config) {
|
|
61
|
+
permissionConfigs.set(config.resource, config);
|
|
62
|
+
}
|
|
63
|
+
// ─── Public API ─────────────────────────────────────────────
|
|
64
|
+
/**
|
|
65
|
+
* Check if a role is allowed to perform an action on a resource.
|
|
66
|
+
*
|
|
67
|
+
* Returns the PermissionValue:
|
|
68
|
+
* - true: allowed
|
|
69
|
+
* - false: denied
|
|
70
|
+
* - 'own': allowed with scope restriction
|
|
71
|
+
* - string: custom scope
|
|
72
|
+
*
|
|
73
|
+
* Unknown roles/resources/actions default to false (fail closed).
|
|
74
|
+
*/
|
|
75
|
+
export function checkPermission(resource, action, role) {
|
|
76
|
+
const config = permissionConfigs.get(resource);
|
|
77
|
+
if (!config)
|
|
78
|
+
return false;
|
|
79
|
+
const rolePerms = config.roles[role];
|
|
80
|
+
if (!rolePerms)
|
|
81
|
+
return false;
|
|
82
|
+
const value = rolePerms[action];
|
|
83
|
+
// Fallback: 'list' inherits from 'read' if not explicitly defined
|
|
84
|
+
if (value === undefined && action === 'list') {
|
|
85
|
+
return rolePerms['read'] ?? false;
|
|
86
|
+
}
|
|
87
|
+
return value ?? false;
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Check if a role can see a nav item for a resource.
|
|
91
|
+
*/
|
|
92
|
+
export function canSeeNav(resource, role) {
|
|
93
|
+
const config = permissionConfigs.get(resource);
|
|
94
|
+
if (!config?.ui?.nav)
|
|
95
|
+
return true; // No nav config = visible to all
|
|
96
|
+
return config.ui.nav.includes(role);
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Check if a role can see a specific tab within a resource.
|
|
100
|
+
*/
|
|
101
|
+
export function canSeeTab(resource, tabId, role) {
|
|
102
|
+
const config = permissionConfigs.get(resource);
|
|
103
|
+
if (!config?.ui?.tabs?.[tabId])
|
|
104
|
+
return true; // No tab config = visible to all
|
|
105
|
+
return config.ui.tabs[tabId].includes(role);
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Check if a role can perform a specific UI action.
|
|
109
|
+
*/
|
|
110
|
+
export function canDoAction(resource, actionId, role) {
|
|
111
|
+
const config = permissionConfigs.get(resource);
|
|
112
|
+
if (!config?.ui?.actions?.[actionId])
|
|
113
|
+
return true; // No action config = visible to all
|
|
114
|
+
return config.ui.actions[actionId].includes(role);
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Get all permission configs. Useful for:
|
|
118
|
+
* - Frontend: GET /api/permissions → returns all UI permissions for the user's role
|
|
119
|
+
* - Documentation: generate permission matrices
|
|
120
|
+
*/
|
|
121
|
+
export function getAllPermissionConfigs() {
|
|
122
|
+
return Object.fromEntries(permissionConfigs);
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Get the resolved permissions for a specific role across all features.
|
|
126
|
+
* Returns only UI-relevant data for the frontend.
|
|
127
|
+
*/
|
|
128
|
+
export function getPermissionsForRole(role) {
|
|
129
|
+
const result = {};
|
|
130
|
+
for (const [resource, config] of permissionConfigs) {
|
|
131
|
+
const rolePerms = config.roles[role] ?? {};
|
|
132
|
+
result[resource] = {
|
|
133
|
+
nav: config.ui?.nav ? config.ui.nav.includes(role) : true,
|
|
134
|
+
tabs: Object.fromEntries(Object.entries(config.ui?.tabs ?? {}).map(([tabId, roles]) => [tabId, roles.includes(role)])),
|
|
135
|
+
actions: Object.fromEntries(Object.entries(config.ui?.actions ?? {}).map(([actionId, roles]) => [actionId, roles.includes(role)])),
|
|
136
|
+
crud: {
|
|
137
|
+
create: rolePerms.create ?? false,
|
|
138
|
+
read: rolePerms.read ?? false,
|
|
139
|
+
update: rolePerms.update ?? false,
|
|
140
|
+
delete: rolePerms.delete ?? false,
|
|
141
|
+
list: rolePerms.list ?? rolePerms.read ?? false,
|
|
142
|
+
},
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
return result;
|
|
146
|
+
}
|
|
147
|
+
// ─── Middleware ──────────────────────────────────────────────
|
|
148
|
+
/**
|
|
149
|
+
* Express middleware: require a specific permission on a resource.
|
|
150
|
+
*
|
|
151
|
+
* Checks the user's role against the registered permission config.
|
|
152
|
+
* Superadmins bypass all permission checks.
|
|
153
|
+
*
|
|
154
|
+
* When the permission value is 'own' or another scope string,
|
|
155
|
+
* the middleware allows the request but attaches `req.permissionScope`
|
|
156
|
+
* so the controller can apply the appropriate filter.
|
|
157
|
+
*
|
|
158
|
+
* @param resource - The resource name (must match a registered config)
|
|
159
|
+
* @param action - The action to check (create, read, update, delete, or custom)
|
|
160
|
+
*
|
|
161
|
+
* @example
|
|
162
|
+
* router.post('/', requirePermission('notes', 'create'), handler);
|
|
163
|
+
* router.delete('/:id', requirePermission('notes', 'delete'), handler);
|
|
164
|
+
*
|
|
165
|
+
* // In controller, check scope:
|
|
166
|
+
* if (req.permissionScope === 'own') {
|
|
167
|
+
* query = query.eq('author_id', req.user.id);
|
|
168
|
+
* }
|
|
169
|
+
*/
|
|
170
|
+
export function requirePermission(resource, action) {
|
|
171
|
+
return (req, res, next) => {
|
|
172
|
+
// Superadmins bypass all permission checks
|
|
173
|
+
if (req.user?.is_superadmin || req.user?.isSuperAdmin || req.user?.isSuperadmin) {
|
|
174
|
+
return next();
|
|
175
|
+
}
|
|
176
|
+
const role = req.user?.role;
|
|
177
|
+
if (!role) {
|
|
178
|
+
return RFC7807ErrorResponse.forbidden(res, 'No role assigned. Cannot check permissions.');
|
|
179
|
+
}
|
|
180
|
+
const permission = checkPermission(resource, action, role);
|
|
181
|
+
if (permission === false) {
|
|
182
|
+
return res.status(403).json({
|
|
183
|
+
type: 'about:blank',
|
|
184
|
+
title: 'Permission denied',
|
|
185
|
+
detail: `Role "${role}" is not allowed to "${action}" on "${resource}".`,
|
|
186
|
+
status: 403,
|
|
187
|
+
instance: req.originalUrl,
|
|
188
|
+
resource,
|
|
189
|
+
action,
|
|
190
|
+
role,
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
// If permission is a scope string (e.g., 'own', 'track_member'),
|
|
194
|
+
// attach it to the request so controllers can filter accordingly.
|
|
195
|
+
if (typeof permission === 'string') {
|
|
196
|
+
req.permissionScope = permission;
|
|
197
|
+
}
|
|
198
|
+
next();
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
/**
|
|
202
|
+
* Express middleware: require ANY of the listed permissions.
|
|
203
|
+
*
|
|
204
|
+
* @example
|
|
205
|
+
* router.get('/feed', requireAnyPermission([
|
|
206
|
+
* { resource: 'notes', action: 'read' },
|
|
207
|
+
* { resource: 'messages', action: 'read' },
|
|
208
|
+
* ]), handler);
|
|
209
|
+
*/
|
|
210
|
+
export function requireAnyPermission(permissions) {
|
|
211
|
+
return (req, res, next) => {
|
|
212
|
+
if (req.user?.is_superadmin || req.user?.isSuperAdmin || req.user?.isSuperadmin) {
|
|
213
|
+
return next();
|
|
214
|
+
}
|
|
215
|
+
const role = req.user?.role;
|
|
216
|
+
if (!role) {
|
|
217
|
+
return RFC7807ErrorResponse.forbidden(res, 'No role assigned.');
|
|
218
|
+
}
|
|
219
|
+
const hasAny = permissions.some(({ resource, action }) => {
|
|
220
|
+
const perm = checkPermission(resource, action, role);
|
|
221
|
+
return perm !== false;
|
|
222
|
+
});
|
|
223
|
+
if (!hasAny) {
|
|
224
|
+
return res.status(403).json({
|
|
225
|
+
type: 'about:blank',
|
|
226
|
+
title: 'Permission denied',
|
|
227
|
+
detail: `Role "${role}" lacks all required permissions.`,
|
|
228
|
+
status: 403,
|
|
229
|
+
instance: req.originalUrl,
|
|
230
|
+
required: permissions,
|
|
231
|
+
role,
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
next();
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
//# sourceMappingURL=permissionsMiddleware.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"permissionsMiddleware.js","sourceRoot":"","sources":["../../src/middleware/permissionsMiddleware.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AffiliateAttributionService
|
|
3
|
+
*
|
|
4
|
+
* Generic affiliate attribution and commission engine.
|
|
5
|
+
* Internal referral only (voucher > URL > cookie).
|
|
6
|
+
* Config-driven tier upgrades.
|
|
7
|
+
*
|
|
8
|
+
* @module @soulbatical/tetra-core/affiliate
|
|
9
|
+
*/
|
|
10
|
+
import type { SupabaseClient } from '@supabase/supabase-js';
|
|
11
|
+
import type { Request } from 'express';
|
|
12
|
+
import type { AffiliateAttribution, AffiliateConfig, AffiliateOrder } from './types.js';
|
|
13
|
+
export declare class AffiliateAttributionService {
|
|
14
|
+
private supabase;
|
|
15
|
+
private organizationId?;
|
|
16
|
+
private config;
|
|
17
|
+
constructor(supabase: SupabaseClient, config?: Partial<AffiliateConfig>, organizationId?: string);
|
|
18
|
+
/**
|
|
19
|
+
* Determine affiliate attribution for an order.
|
|
20
|
+
* Priority: Voucher > URL (?ref=) > Cookie
|
|
21
|
+
*/
|
|
22
|
+
determineAttribution(req: Request, voucherCode?: string): Promise<AffiliateAttribution | null>;
|
|
23
|
+
/**
|
|
24
|
+
* Create a commission record for an affiliate sale.
|
|
25
|
+
* Updates affiliate stats and checks for tier upgrade.
|
|
26
|
+
*/
|
|
27
|
+
createCommission(order: AffiliateOrder, attribution: AffiliateAttribution): Promise<void>;
|
|
28
|
+
/**
|
|
29
|
+
* Generate a referral code for a new affiliate.
|
|
30
|
+
* Uses custom generator from config or default format.
|
|
31
|
+
*/
|
|
32
|
+
generateReferralCode(contactName: string): string;
|
|
33
|
+
private checkVoucherAttribution;
|
|
34
|
+
private checkUrlParameter;
|
|
35
|
+
private checkCookie;
|
|
36
|
+
/**
|
|
37
|
+
* Resolve an affiliate from referral code or ID.
|
|
38
|
+
*/
|
|
39
|
+
resolveAffiliate(reference: string, affiliateId: string | null): Promise<any>;
|
|
40
|
+
private updateAffiliateStats;
|
|
41
|
+
/**
|
|
42
|
+
* Check if affiliate qualifies for tier upgrade (config-driven).
|
|
43
|
+
*/
|
|
44
|
+
checkTierUpgrade(affiliateId: string, totalSales: number): Promise<void>;
|
|
45
|
+
private upgradeAffiliateTier;
|
|
46
|
+
}
|
|
47
|
+
//# sourceMappingURL=AffiliateAttributionService.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"AffiliateAttributionService.d.ts","sourceRoot":"","sources":["../../../src/shared/affiliate/AffiliateAttributionService.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AAC5D,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAGvC,OAAO,KAAK,EAAE,oBAAoB,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AA0BxF,qBAAa,2BAA2B;IACtC,OAAO,CAAC,QAAQ,CAAiB;IACjC,OAAO,CAAC,cAAc,CAAC,CAAS;IAChC,OAAO,CAAC,MAAM,CAAkB;gBAG9B,QAAQ,EAAE,cAAc,EACxB,MAAM,CAAC,EAAE,OAAO,CAAC,eAAe,CAAC,EACjC,cAAc,CAAC,EAAE,MAAM;IASzB;;;OAGG;IACG,oBAAoB,CACxB,GAAG,EAAE,OAAO,EACZ,WAAW,CAAC,EAAE,MAAM,GACnB,OAAO,CAAC,oBAAoB,GAAG,IAAI,CAAC;IAgCvC;;;OAGG;IACG,gBAAgB,CACpB,KAAK,EAAE,cAAc,EACrB,WAAW,EAAE,oBAAoB,GAChC,OAAO,CAAC,IAAI,CAAC;IA6DhB;;;OAGG;IACH,oBAAoB,CAAC,WAAW,EAAE,MAAM,GAAG,MAAM;YAOnC,uBAAuB;IAwBrC,OAAO,CAAC,iBAAiB;IAYzB,OAAO,CAAC,WAAW;IAiCnB;;OAEG;IACG,gBAAgB,CACpB,SAAS,EAAE,MAAM,EACjB,WAAW,EAAE,MAAM,GAAG,IAAI,GACzB,OAAO,CAAC,GAAG,CAAC;YAkCD,oBAAoB;IAgBlC;;OAEG;IACG,gBAAgB,CAAC,WAAW,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;YA8ChE,oBAAoB;CAkDnC"}
|
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
import crypto from 'crypto';
|
|
2
|
+
import { createLogger } from '../../utils/logger.js';
|
|
3
|
+
const logger = createLogger('service:affiliate:attribution');
|
|
4
|
+
const DEFAULT_CONFIG = {
|
|
5
|
+
defaultCommissionPercentage: 30,
|
|
6
|
+
cookieDurationDays: 90,
|
|
7
|
+
tiers: [
|
|
8
|
+
{ name: 'starter', minSales: 0, commissionPercentage: 30 },
|
|
9
|
+
{ name: 'active', minSales: 10, commissionPercentage: 35 },
|
|
10
|
+
],
|
|
11
|
+
};
|
|
12
|
+
/**
|
|
13
|
+
* Generate a referral code from a contact name.
|
|
14
|
+
* Format: firstname-XXXX (e.g., 'monique-1234')
|
|
15
|
+
*/
|
|
16
|
+
function defaultReferralCodeGenerator(contactName) {
|
|
17
|
+
const normalized = contactName
|
|
18
|
+
.toLowerCase()
|
|
19
|
+
.replace(/[^a-z0-9]/g, '')
|
|
20
|
+
.substring(0, 15);
|
|
21
|
+
const randomDigits = crypto.randomInt(1000, 10000);
|
|
22
|
+
return `${normalized}-${randomDigits}`;
|
|
23
|
+
}
|
|
24
|
+
export class AffiliateAttributionService {
|
|
25
|
+
supabase;
|
|
26
|
+
organizationId;
|
|
27
|
+
config;
|
|
28
|
+
constructor(supabase, config, organizationId) {
|
|
29
|
+
this.supabase = supabase;
|
|
30
|
+
this.organizationId = organizationId;
|
|
31
|
+
this.config = { ...DEFAULT_CONFIG, ...config };
|
|
32
|
+
}
|
|
33
|
+
// ─── Attribution ────────────────────────────────────────────
|
|
34
|
+
/**
|
|
35
|
+
* Determine affiliate attribution for an order.
|
|
36
|
+
* Priority: Voucher > URL (?ref=) > Cookie
|
|
37
|
+
*/
|
|
38
|
+
async determineAttribution(req, voucherCode) {
|
|
39
|
+
logger.info('Determining affiliate attribution', { voucherCode });
|
|
40
|
+
// 1. Voucher with affiliate
|
|
41
|
+
if (voucherCode) {
|
|
42
|
+
const attribution = await this.checkVoucherAttribution(voucherCode);
|
|
43
|
+
if (attribution) {
|
|
44
|
+
logger.info('Attribution via voucher', attribution);
|
|
45
|
+
return attribution;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
// 2. URL parameter (?ref=)
|
|
49
|
+
const urlAttribution = this.checkUrlParameter(req);
|
|
50
|
+
if (urlAttribution) {
|
|
51
|
+
logger.info('Attribution via URL', urlAttribution);
|
|
52
|
+
return urlAttribution;
|
|
53
|
+
}
|
|
54
|
+
// 3. Cookie fallback
|
|
55
|
+
const cookieAttribution = this.checkCookie(req);
|
|
56
|
+
if (cookieAttribution) {
|
|
57
|
+
logger.info('Attribution via cookie', cookieAttribution);
|
|
58
|
+
return cookieAttribution;
|
|
59
|
+
}
|
|
60
|
+
logger.info('No affiliate attribution found');
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
// ─── Commission ─────────────────────────────────────────────
|
|
64
|
+
/**
|
|
65
|
+
* Create a commission record for an affiliate sale.
|
|
66
|
+
* Updates affiliate stats and checks for tier upgrade.
|
|
67
|
+
*/
|
|
68
|
+
async createCommission(order, attribution) {
|
|
69
|
+
logger.info('Creating commission', { orderId: order.id, attribution });
|
|
70
|
+
const affiliate = await this.resolveAffiliate(attribution.reference, attribution.affiliate_id);
|
|
71
|
+
if (!affiliate) {
|
|
72
|
+
logger.warn('Affiliate not found', { attribution });
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
const orderAmount = order.total_excl_vat;
|
|
76
|
+
const commissionAmount = orderAmount * (affiliate.commission_percentage / 100);
|
|
77
|
+
logger.info('Commission calculated', {
|
|
78
|
+
affiliateId: affiliate.id,
|
|
79
|
+
orderAmount,
|
|
80
|
+
commissionPercentage: affiliate.commission_percentage,
|
|
81
|
+
commissionAmount,
|
|
82
|
+
});
|
|
83
|
+
// Create commission record
|
|
84
|
+
const { data: commission, error } = await this.supabase
|
|
85
|
+
.from('affiliate_commissions')
|
|
86
|
+
.insert({
|
|
87
|
+
organization_id: order.organization_id,
|
|
88
|
+
affiliate_id: affiliate.id,
|
|
89
|
+
order_id: order.id,
|
|
90
|
+
affiliate_source: 'internal',
|
|
91
|
+
product_name: 'Order',
|
|
92
|
+
order_amount_excl_vat: orderAmount,
|
|
93
|
+
commission_percentage: affiliate.commission_percentage,
|
|
94
|
+
commission_amount: commissionAmount,
|
|
95
|
+
status: 'pending',
|
|
96
|
+
})
|
|
97
|
+
.select()
|
|
98
|
+
.single();
|
|
99
|
+
if (error) {
|
|
100
|
+
logger.error('Failed to create commission', { error, orderId: order.id });
|
|
101
|
+
throw error;
|
|
102
|
+
}
|
|
103
|
+
logger.info('Commission created', { commissionId: commission.id });
|
|
104
|
+
// Update affiliate stats
|
|
105
|
+
await this.updateAffiliateStats(affiliate.id, {
|
|
106
|
+
total_sales: affiliate.total_sales + 1,
|
|
107
|
+
total_revenue: affiliate.total_revenue + orderAmount,
|
|
108
|
+
total_commission_earned: affiliate.total_commission_earned + commissionAmount,
|
|
109
|
+
last_sale_at: new Date().toISOString(),
|
|
110
|
+
});
|
|
111
|
+
// Check for tier upgrade
|
|
112
|
+
await this.checkTierUpgrade(affiliate.id, affiliate.total_sales + 1);
|
|
113
|
+
}
|
|
114
|
+
// ─── Referral Code ──────────────────────────────────────────
|
|
115
|
+
/**
|
|
116
|
+
* Generate a referral code for a new affiliate.
|
|
117
|
+
* Uses custom generator from config or default format.
|
|
118
|
+
*/
|
|
119
|
+
generateReferralCode(contactName) {
|
|
120
|
+
const generator = this.config.referralCodeGenerator || defaultReferralCodeGenerator;
|
|
121
|
+
return generator(contactName);
|
|
122
|
+
}
|
|
123
|
+
// ─── Private: Attribution Checks ────────────────────────────
|
|
124
|
+
async checkVoucherAttribution(voucherCode) {
|
|
125
|
+
try {
|
|
126
|
+
const { data: voucher, error } = await this.supabase
|
|
127
|
+
.from('vouchers')
|
|
128
|
+
.select('id, affiliate_id')
|
|
129
|
+
.eq('code', voucherCode)
|
|
130
|
+
.single();
|
|
131
|
+
if (error || !voucher?.affiliate_id) {
|
|
132
|
+
return null;
|
|
133
|
+
}
|
|
134
|
+
return {
|
|
135
|
+
affiliate_id: voucher.affiliate_id,
|
|
136
|
+
source: 'internal',
|
|
137
|
+
attribution_type: 'voucher',
|
|
138
|
+
reference: voucherCode,
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
catch (error) {
|
|
142
|
+
logger.error('Error checking voucher attribution', error);
|
|
143
|
+
return null;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
checkUrlParameter(req) {
|
|
147
|
+
const ref = req.query.ref;
|
|
148
|
+
if (!ref)
|
|
149
|
+
return null;
|
|
150
|
+
return {
|
|
151
|
+
affiliate_id: null,
|
|
152
|
+
source: 'internal',
|
|
153
|
+
attribution_type: 'url',
|
|
154
|
+
reference: ref,
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
checkCookie(req) {
|
|
158
|
+
const cookie = req.cookies?.affiliate_ref;
|
|
159
|
+
if (!cookie)
|
|
160
|
+
return null;
|
|
161
|
+
try {
|
|
162
|
+
const data = JSON.parse(cookie);
|
|
163
|
+
const age = Date.now() - data.timestamp;
|
|
164
|
+
const maxAge = this.config.cookieDurationDays * 24 * 60 * 60 * 1000;
|
|
165
|
+
if (age > maxAge) {
|
|
166
|
+
logger.info('Affiliate cookie expired', { ageDays: Math.floor(age / (24 * 60 * 60 * 1000)) });
|
|
167
|
+
return null;
|
|
168
|
+
}
|
|
169
|
+
// Only accept internal source in tetra-core
|
|
170
|
+
if (data.source && data.source !== 'internal') {
|
|
171
|
+
return null;
|
|
172
|
+
}
|
|
173
|
+
return {
|
|
174
|
+
affiliate_id: null,
|
|
175
|
+
source: 'internal',
|
|
176
|
+
attribution_type: 'cookie',
|
|
177
|
+
reference: data.ref,
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
catch (error) {
|
|
181
|
+
logger.error('Error parsing affiliate cookie', error);
|
|
182
|
+
return null;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
// ─── Private: Affiliate Resolution ──────────────────────────
|
|
186
|
+
/**
|
|
187
|
+
* Resolve an affiliate from referral code or ID.
|
|
188
|
+
*/
|
|
189
|
+
async resolveAffiliate(reference, affiliateId) {
|
|
190
|
+
// If affiliate ID already known (voucher attribution)
|
|
191
|
+
if (affiliateId) {
|
|
192
|
+
const { data, error } = await this.supabase
|
|
193
|
+
.from('affiliates')
|
|
194
|
+
.select('*')
|
|
195
|
+
.eq('id', affiliateId)
|
|
196
|
+
.eq('status', 'active')
|
|
197
|
+
.single();
|
|
198
|
+
if (error) {
|
|
199
|
+
logger.error('Failed to fetch affiliate by ID', { error, affiliateId });
|
|
200
|
+
return null;
|
|
201
|
+
}
|
|
202
|
+
return data;
|
|
203
|
+
}
|
|
204
|
+
// Look up by referral code
|
|
205
|
+
const { data, error } = await this.supabase
|
|
206
|
+
.from('affiliates')
|
|
207
|
+
.select('*')
|
|
208
|
+
.eq('referral_code', reference)
|
|
209
|
+
.eq('status', 'active')
|
|
210
|
+
.single();
|
|
211
|
+
if (error) {
|
|
212
|
+
logger.error('Failed to fetch affiliate by referral code', { error, reference });
|
|
213
|
+
return null;
|
|
214
|
+
}
|
|
215
|
+
return data;
|
|
216
|
+
}
|
|
217
|
+
// ─── Private: Stats & Tiers ─────────────────────────────────
|
|
218
|
+
async updateAffiliateStats(affiliateId, updates) {
|
|
219
|
+
const { error } = await this.supabase
|
|
220
|
+
.from('affiliates')
|
|
221
|
+
.update(updates)
|
|
222
|
+
.eq('id', affiliateId);
|
|
223
|
+
if (error) {
|
|
224
|
+
logger.error('Failed to update affiliate stats', { error, affiliateId });
|
|
225
|
+
throw error;
|
|
226
|
+
}
|
|
227
|
+
logger.info('Affiliate stats updated', { affiliateId, updates });
|
|
228
|
+
}
|
|
229
|
+
/**
|
|
230
|
+
* Check if affiliate qualifies for tier upgrade (config-driven).
|
|
231
|
+
*/
|
|
232
|
+
async checkTierUpgrade(affiliateId, totalSales) {
|
|
233
|
+
const { data: affiliate, error } = await this.supabase
|
|
234
|
+
.from('affiliates')
|
|
235
|
+
.select('tier, commission_percentage')
|
|
236
|
+
.eq('id', affiliateId)
|
|
237
|
+
.single();
|
|
238
|
+
if (error || !affiliate) {
|
|
239
|
+
logger.error('Failed to fetch affiliate for tier check', { error, affiliateId });
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
// Find the highest qualifying tier
|
|
243
|
+
const sortedTiers = [...this.config.tiers].sort((a, b) => b.minSales - a.minSales);
|
|
244
|
+
const qualifyingTier = sortedTiers.find(t => totalSales >= t.minSales);
|
|
245
|
+
if (!qualifyingTier || qualifyingTier.name === affiliate.tier) {
|
|
246
|
+
return; // No upgrade needed
|
|
247
|
+
}
|
|
248
|
+
// Check if this is actually an upgrade (not a downgrade)
|
|
249
|
+
const currentTierIndex = this.config.tiers.findIndex(t => t.name === affiliate.tier);
|
|
250
|
+
const newTierIndex = this.config.tiers.findIndex(t => t.name === qualifyingTier.name);
|
|
251
|
+
if (newTierIndex <= currentTierIndex) {
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
logger.info('Tier upgrade triggered', {
|
|
255
|
+
affiliateId,
|
|
256
|
+
oldTier: affiliate.tier,
|
|
257
|
+
newTier: qualifyingTier.name,
|
|
258
|
+
oldCommission: affiliate.commission_percentage,
|
|
259
|
+
newCommission: qualifyingTier.commissionPercentage,
|
|
260
|
+
totalSales,
|
|
261
|
+
});
|
|
262
|
+
await this.upgradeAffiliateTier(affiliateId, {
|
|
263
|
+
old_tier: affiliate.tier,
|
|
264
|
+
new_tier: qualifyingTier.name,
|
|
265
|
+
old_commission: affiliate.commission_percentage,
|
|
266
|
+
new_commission: qualifyingTier.commissionPercentage,
|
|
267
|
+
reason: 'milestone_reached',
|
|
268
|
+
triggered_by_sale_count: totalSales,
|
|
269
|
+
});
|
|
270
|
+
}
|
|
271
|
+
async upgradeAffiliateTier(affiliateId, upgrade) {
|
|
272
|
+
// Update affiliate tier and commission
|
|
273
|
+
const { error: updateError } = await this.supabase
|
|
274
|
+
.from('affiliates')
|
|
275
|
+
.update({
|
|
276
|
+
tier: upgrade.new_tier,
|
|
277
|
+
commission_percentage: upgrade.new_commission,
|
|
278
|
+
updated_at: new Date().toISOString(),
|
|
279
|
+
})
|
|
280
|
+
.eq('id', affiliateId);
|
|
281
|
+
if (updateError) {
|
|
282
|
+
logger.error('Failed to update affiliate tier', { error: updateError, affiliateId });
|
|
283
|
+
throw updateError;
|
|
284
|
+
}
|
|
285
|
+
// Record in tier history
|
|
286
|
+
const { error: historyError } = await this.supabase
|
|
287
|
+
.from('affiliate_tier_history')
|
|
288
|
+
.insert({
|
|
289
|
+
affiliate_id: affiliateId,
|
|
290
|
+
old_tier: upgrade.old_tier,
|
|
291
|
+
new_tier: upgrade.new_tier,
|
|
292
|
+
old_commission_percentage: upgrade.old_commission,
|
|
293
|
+
new_commission_percentage: upgrade.new_commission,
|
|
294
|
+
reason: upgrade.reason,
|
|
295
|
+
triggered_by_sale_count: upgrade.triggered_by_sale_count,
|
|
296
|
+
});
|
|
297
|
+
if (historyError) {
|
|
298
|
+
logger.error('Failed to record tier history', { error: historyError, affiliateId });
|
|
299
|
+
// Don't throw — tier upgrade succeeded even if history failed
|
|
300
|
+
}
|
|
301
|
+
logger.info('Tier upgraded successfully', {
|
|
302
|
+
affiliateId,
|
|
303
|
+
oldTier: upgrade.old_tier,
|
|
304
|
+
newTier: upgrade.new_tier,
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
//# sourceMappingURL=AffiliateAttributionService.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"AffiliateAttributionService.js","sourceRoot":"","sources":["../../../src/shared/affiliate/AffiliateAttributionService.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AffiliateClickService
|
|
3
|
+
*
|
|
4
|
+
* Handles click tracking and conversion marking for the affiliate module.
|
|
5
|
+
*
|
|
6
|
+
* @module @soulbatical/tetra-core/affiliate
|
|
7
|
+
*/
|
|
8
|
+
import type { SupabaseClient } from '@supabase/supabase-js';
|
|
9
|
+
export declare class AffiliateClickService {
|
|
10
|
+
private supabase;
|
|
11
|
+
constructor(supabase: SupabaseClient);
|
|
12
|
+
/**
|
|
13
|
+
* Register an affiliate click event.
|
|
14
|
+
* Returns the click record ID.
|
|
15
|
+
*/
|
|
16
|
+
registerClick(data: {
|
|
17
|
+
affiliate_id?: string | null;
|
|
18
|
+
organization_id?: string | null;
|
|
19
|
+
affiliate_source: string;
|
|
20
|
+
visitor_ip?: string | null;
|
|
21
|
+
user_agent?: string | null;
|
|
22
|
+
referrer_url?: string | null;
|
|
23
|
+
landing_page?: string | null;
|
|
24
|
+
external_click_ref?: string | null;
|
|
25
|
+
}): Promise<string | null>;
|
|
26
|
+
/**
|
|
27
|
+
* Increment affiliate total_clicks counter.
|
|
28
|
+
*/
|
|
29
|
+
incrementClickCount(affiliateId: string): Promise<void>;
|
|
30
|
+
/**
|
|
31
|
+
* Mark a click as converted after a successful order.
|
|
32
|
+
*/
|
|
33
|
+
markConverted(clickId: string, orderId: string): Promise<void>;
|
|
34
|
+
}
|
|
35
|
+
//# sourceMappingURL=AffiliateClickService.d.ts.map
|