@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.

Files changed (37) hide show
  1. package/dist/index.d.ts +9 -0
  2. package/dist/index.d.ts.map +1 -1
  3. package/dist/index.js +9 -0
  4. package/dist/index.js.map +1 -1
  5. package/dist/middleware/entitlementsMiddleware.d.ts +142 -0
  6. package/dist/middleware/entitlementsMiddleware.d.ts.map +1 -0
  7. package/dist/middleware/entitlementsMiddleware.js +246 -0
  8. package/dist/middleware/entitlementsMiddleware.js.map +1 -0
  9. package/dist/middleware/permissionsMiddleware.d.ts +181 -0
  10. package/dist/middleware/permissionsMiddleware.d.ts.map +1 -0
  11. package/dist/middleware/permissionsMiddleware.js +237 -0
  12. package/dist/middleware/permissionsMiddleware.js.map +1 -0
  13. package/dist/shared/affiliate/AffiliateAttributionService.d.ts +47 -0
  14. package/dist/shared/affiliate/AffiliateAttributionService.d.ts.map +1 -0
  15. package/dist/shared/affiliate/AffiliateAttributionService.js +308 -0
  16. package/dist/shared/affiliate/AffiliateAttributionService.js.map +1 -0
  17. package/dist/shared/affiliate/AffiliateClickService.d.ts +35 -0
  18. package/dist/shared/affiliate/AffiliateClickService.d.ts.map +1 -0
  19. package/dist/shared/affiliate/AffiliateClickService.js +87 -0
  20. package/dist/shared/affiliate/AffiliateClickService.js.map +1 -0
  21. package/dist/shared/affiliate/affiliateFeatureConfig.d.ts +11 -0
  22. package/dist/shared/affiliate/affiliateFeatureConfig.d.ts.map +1 -0
  23. package/dist/shared/affiliate/affiliateFeatureConfig.js +242 -0
  24. package/dist/shared/affiliate/affiliateFeatureConfig.js.map +1 -0
  25. package/dist/shared/affiliate/index.d.ts +11 -0
  26. package/dist/shared/affiliate/index.d.ts.map +1 -0
  27. package/dist/shared/affiliate/index.js +13 -0
  28. package/dist/shared/affiliate/index.js.map +1 -0
  29. package/dist/shared/affiliate/routes.d.ts +87 -0
  30. package/dist/shared/affiliate/routes.d.ts.map +1 -0
  31. package/dist/shared/affiliate/routes.js +404 -0
  32. package/dist/shared/affiliate/routes.js.map +1 -0
  33. package/dist/shared/affiliate/types.d.ts +170 -0
  34. package/dist/shared/affiliate/types.d.ts.map +1 -0
  35. package/dist/shared/affiliate/types.js +11 -0
  36. package/dist/shared/affiliate/types.js.map +1 -0
  37. 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