elysium-attribution 1.0.0 → 1.0.2

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.
@@ -0,0 +1,112 @@
1
+ import { describe, it } from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import {
4
+ GatewayRegistry,
5
+ PaymentGateway,
6
+ StripeGateway,
7
+ RazorpayGateway,
8
+ PayPalGateway,
9
+ GenericGateway,
10
+ } from '../src/gateways/index.js';
11
+
12
+ describe('Payment Gateways Architecture', () => {
13
+ it('manages default and custom gateways in GatewayRegistry', () => {
14
+ const registry = new GatewayRegistry();
15
+
16
+ assert.ok(registry.get('stripe') instanceof StripeGateway);
17
+ assert.ok(registry.get('razorpay') instanceof RazorpayGateway);
18
+ assert.ok(registry.get('paypal') instanceof PayPalGateway);
19
+
20
+ // Register custom crypto gateway
21
+ class CryptoGateway extends PaymentGateway {
22
+ constructor() {
23
+ super('crypto');
24
+ }
25
+ }
26
+ registry.register('crypto', new CryptoGateway());
27
+ assert.ok(registry.get('crypto') instanceof CryptoGateway);
28
+ });
29
+
30
+ it('PaymentGateway emits lifecycle events on attempt, success, and failure', async () => {
31
+ const gateway = new GenericGateway('testgw');
32
+ const events = [];
33
+
34
+ gateway.on('attempt', (e) => events.push({ type: 'attempt', ...e }));
35
+ gateway.on('success', (e) => events.push({ type: 'success', ...e }));
36
+ gateway.on('failure', (e) => events.push({ type: 'failure', ...e }));
37
+
38
+ await gateway.recordPaymentAttempt({ stage: 'init' });
39
+ await gateway.recordPaymentSuccess({ transactionId: 'tx_999', amount: 150 });
40
+ await gateway.recordPaymentFailure({ code: 'card_declined', message: 'Insufficient funds' });
41
+
42
+ assert.equal(events.length, 3);
43
+ assert.equal(events[0].type, 'attempt');
44
+ assert.equal(events[1].type, 'success');
45
+ assert.equal(events[1].transactionId, 'tx_999');
46
+ assert.equal(events[2].type, 'failure');
47
+ assert.equal(events[2].errorCode, 'card_declined');
48
+ });
49
+
50
+ it('RazorpayGateway wraps options and intercepts success handler', async () => {
51
+ const rzpGateway = new RazorpayGateway();
52
+ let originalHandlerCalled = false;
53
+ let recordedSuccess = null;
54
+
55
+ rzpGateway.on('success', (data) => {
56
+ recordedSuccess = data;
57
+ });
58
+
59
+ const originalOptions = {
60
+ key: 'rzp_test_123',
61
+ amount: 50000,
62
+ currency: 'INR',
63
+ handler: (res) => {
64
+ originalHandlerCalled = true;
65
+ },
66
+ };
67
+
68
+ const wrapped = rzpGateway.wrapOptions(originalOptions);
69
+ assert.notEqual(wrapped.handler, originalOptions.handler);
70
+
71
+ // Simulate Razorpay calling the handler upon customer completing payment
72
+ wrapped.handler({
73
+ razorpay_payment_id: 'pay_ABC123',
74
+ razorpay_order_id: 'order_XYZ456',
75
+ razorpay_signature: 'sig_789',
76
+ });
77
+
78
+ assert.equal(originalHandlerCalled, true);
79
+ assert.ok(recordedSuccess);
80
+ assert.equal(recordedSuccess.transactionId, 'pay_ABC123');
81
+ assert.equal(recordedSuccess.orderId, 'order_XYZ456');
82
+ assert.equal(recordedSuccess.amount, 500); // 50000 paise -> 500 INR
83
+ });
84
+
85
+ it('PayPalGateway wraps buttons and intercepts onApprove', async () => {
86
+ const ppGateway = new PayPalGateway();
87
+ let originalApproveCalled = false;
88
+ let recordedSuccess = null;
89
+
90
+ ppGateway.on('success', (data) => {
91
+ recordedSuccess = data;
92
+ });
93
+
94
+ const originalButtonOpts = {
95
+ createOrder: () => 'order_pp_1',
96
+ onApprove: (data) => {
97
+ originalApproveCalled = true;
98
+ },
99
+ };
100
+
101
+ const wrapped = ppGateway.wrapButtons(originalButtonOpts);
102
+ await wrapped.onApprove({
103
+ orderID: 'PP_ORDER_999',
104
+ payerID: 'PAYER_555',
105
+ });
106
+
107
+ assert.equal(originalApproveCalled, true);
108
+ assert.ok(recordedSuccess);
109
+ assert.equal(recordedSuccess.orderId, 'PP_ORDER_999');
110
+ assert.equal(recordedSuccess.payerId, 'PAYER_555');
111
+ });
112
+ });
@@ -0,0 +1,51 @@
1
+ import { describe, it } from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import { extractMarketingParams } from '../src/tracking/params.js';
4
+ import { configManager } from '../src/config/index.js';
5
+
6
+ describe('Marketing Parameters Extraction Module', () => {
7
+ it('extracts standard UTM parameters', () => {
8
+ const search = '?utm_source=google&utm_medium=cpc&utm_campaign=summer_sale&utm_term=shoes&utm_content=banner_top';
9
+ const result = extractMarketingParams(search);
10
+
11
+ assert.equal(result.utm.source, 'google');
12
+ assert.equal(result.utm.medium, 'cpc');
13
+ assert.equal(result.utm.campaign, 'summer_sale');
14
+ assert.equal(result.utm.term, 'shoes');
15
+ assert.equal(result.utm.content, 'banner_top');
16
+ });
17
+
18
+ it('falls back to short query parameters if utm_ prefix is omitted', () => {
19
+ const search = '?source=newsletter&medium=email&campaign=welcome_flow';
20
+ const result = extractMarketingParams(search);
21
+
22
+ assert.equal(result.utm.source, 'newsletter');
23
+ assert.equal(result.utm.medium, 'email');
24
+ assert.equal(result.utm.campaign, 'welcome_flow');
25
+ });
26
+
27
+ it('extracts multi-platform ad click IDs', () => {
28
+ const search = '?gclid=test_gclid_123&fbclid=test_fbclid_456&ttclid=test_ttclid_789&msclkid=test_msclkid_101&wbraid=test_wbraid_202&li_fat_id=test_linkedin_303';
29
+ const result = extractMarketingParams(search);
30
+
31
+ assert.equal(result.clickIds.gclid, 'test_gclid_123');
32
+ assert.equal(result.clickIds.fbclid, 'test_fbclid_456');
33
+ assert.equal(result.clickIds.ttclid, 'test_ttclid_789');
34
+ assert.equal(result.clickIds.msclkid, 'test_msclkid_101');
35
+ assert.equal(result.clickIds.wbraid, 'test_wbraid_202');
36
+ assert.equal(result.clickIds.li_fat_id, 'test_linkedin_303');
37
+ });
38
+
39
+ it('extracts configured custom query parameters', () => {
40
+ configManager.init({
41
+ customParams: ['affiliate_id', 'promo_code'],
42
+ });
43
+
44
+ const search = '?affiliate_id=aff_9988&promo_code=SAVE20&other=ignored';
45
+ const result = extractMarketingParams(search);
46
+
47
+ assert.equal(result.custom.affiliate_id, 'aff_9988');
48
+ assert.equal(result.custom.promo_code, 'SAVE20');
49
+ assert.equal(result.custom.other, undefined);
50
+ });
51
+ });
@@ -0,0 +1,41 @@
1
+ import { describe, it } from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import { isInternalReferrer, parseReferrer } from '../src/tracking/referrer.js';
4
+
5
+ describe('Referrer Parsing and Classification Module', () => {
6
+ it('identifies internal referrals correctly', () => {
7
+ assert.equal(isInternalReferrer('https://mystore.com/catalog', 'mystore.com'), true);
8
+ assert.equal(isInternalReferrer('https://shop.mystore.com/cart', 'mystore.com'), true);
9
+ assert.equal(isInternalReferrer('https://google.com', 'mystore.com'), false);
10
+ assert.equal(isInternalReferrer('', 'mystore.com'), false);
11
+ });
12
+
13
+ it('classifies organic search engines', () => {
14
+ const googleResult = parseReferrer('https://www.google.com/search?q=shoes', 'mystore.com');
15
+ assert.equal(googleResult.channel, 'organic_search');
16
+ assert.equal(googleResult.detectedSource, 'google');
17
+ assert.equal(googleResult.detectedMedium, 'organic');
18
+
19
+ const bingResult = parseReferrer('https://www.bing.com/', 'mystore.com');
20
+ assert.equal(bingResult.channel, 'organic_search');
21
+ assert.equal(bingResult.detectedSource, 'bing');
22
+ });
23
+
24
+ it('classifies social networks', () => {
25
+ const fbResult = parseReferrer('https://l.facebook.com/', 'mystore.com');
26
+ assert.equal(fbResult.channel, 'social');
27
+ assert.equal(fbResult.detectedSource, 'facebook');
28
+ assert.equal(fbResult.detectedMedium, 'social');
29
+
30
+ const redditResult = parseReferrer('https://www.reddit.com/r/deals', 'mystore.com');
31
+ assert.equal(redditResult.channel, 'social');
32
+ assert.equal(redditResult.detectedSource, 'reddit');
33
+ });
34
+
35
+ it('filters out internal referrers and marks channel as internal', () => {
36
+ const internalResult = parseReferrer('https://mystore.com/products', 'mystore.com');
37
+ assert.equal(internalResult.isInternal, true);
38
+ assert.equal(internalResult.referrer, null);
39
+ assert.equal(internalResult.channel, 'internal');
40
+ });
41
+ });
@@ -0,0 +1,112 @@
1
+ import { describe, it, beforeEach } from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import {
4
+ shouldSendTouch,
5
+ saveSuccessfulTouch,
6
+ readTouchHistory,
7
+ TOUCH_HISTORY_KEY,
8
+ getTouchSignature,
9
+ } from '../src/tracking/session.js';
10
+ import { safeLocalStorage } from '../src/utils/storage.js';
11
+ import { configManager } from '../src/config/index.js';
12
+
13
+ describe('Session Tracking and Deduplication Module', () => {
14
+ beforeEach(() => {
15
+ safeLocalStorage.removeItem(TOUCH_HISTORY_KEY);
16
+ configManager.init({
17
+ sessionWindowMs: 30 * 60 * 1000,
18
+ });
19
+ });
20
+
21
+ it('allows the initial touch when history is empty', () => {
22
+ const payload = {
23
+ visitorId: 'vis-1',
24
+ source: 'google',
25
+ medium: 'cpc',
26
+ campaign: 'shoes',
27
+ };
28
+ assert.equal(shouldSendTouch(payload, []), true);
29
+ });
30
+
31
+ it('rejects duplicate touch with the same signature within the session window', () => {
32
+ const payload = {
33
+ visitorId: 'vis-1',
34
+ source: 'google',
35
+ medium: 'cpc',
36
+ campaign: 'shoes',
37
+ landingPage: 'https://mystore.com/shoes',
38
+ };
39
+
40
+ const history = [
41
+ {
42
+ payload,
43
+ response: { id: 't-1', visitorId: 'vis-1', channel: 'cpc', touchedAt: '' },
44
+ recordedAt: Date.now() - 5000, // 5 seconds ago
45
+ },
46
+ ];
47
+
48
+ assert.equal(shouldSendTouch(payload, history), false);
49
+ });
50
+
51
+ it('protects paid marketing attribution from being overwritten by a direct navigation in the same session', () => {
52
+ const adPayload = {
53
+ visitorId: 'vis-1',
54
+ source: 'facebook',
55
+ medium: 'cpc',
56
+ campaign: 'retargeting',
57
+ fbclid: 'fb_123',
58
+ };
59
+
60
+ const directPayload = {
61
+ visitorId: 'vis-1',
62
+ source: null,
63
+ medium: null,
64
+ campaign: null,
65
+ };
66
+
67
+ const history = [
68
+ {
69
+ payload: adPayload,
70
+ response: { id: 't-1', visitorId: 'vis-1', channel: 'cpc', touchedAt: '' },
71
+ recordedAt: Date.now() - 60000, // 1 min ago
72
+ },
73
+ ];
74
+
75
+ // Direct navigation should not overwrite the active paid touch
76
+ assert.equal(shouldSendTouch(directPayload, history), false);
77
+ });
78
+
79
+ it('allows a new touch when session window has expired', () => {
80
+ const payload = {
81
+ visitorId: 'vis-1',
82
+ source: 'google',
83
+ medium: 'cpc',
84
+ };
85
+
86
+ const history = [
87
+ {
88
+ payload,
89
+ response: { id: 't-1', visitorId: 'vis-1', channel: 'cpc', touchedAt: '' },
90
+ recordedAt: Date.now() - (35 * 60 * 1000), // 35 minutes ago (> 30 min window)
91
+ },
92
+ ];
93
+
94
+ assert.equal(shouldSendTouch(payload, history), true);
95
+ });
96
+
97
+ it('limits saved history to 50 items and purges older records (FIFO)', () => {
98
+ for (let i = 0; i < 60; i++) {
99
+ saveSuccessfulTouch({
100
+ payload: { visitorId: 'vis-1', index: i },
101
+ response: { id: `touch-${i}`, visitorId: 'vis-1', channel: 'direct', touchedAt: '' },
102
+ recordedAt: Date.now() + i,
103
+ });
104
+ }
105
+
106
+ const history = readTouchHistory();
107
+ assert.equal(history.length, 50);
108
+ // Oldest 10 items (0..9) should be dropped, item 10 should be the first
109
+ assert.equal(history[0].payload.index, 10);
110
+ assert.equal(history[49].payload.index, 59);
111
+ });
112
+ });
@@ -0,0 +1,35 @@
1
+ import { describe, it, beforeEach } from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import { generateUUID, getVisitorId, setVisitorId, VISITOR_ID_KEY } from '../src/tracking/visitor.js';
4
+ import { safeLocalStorage } from '../src/utils/storage.js';
5
+
6
+ describe('Visitor Tracking Module', () => {
7
+ beforeEach(() => {
8
+ safeLocalStorage.removeItem(VISITOR_ID_KEY);
9
+ // Reset memory cache if any
10
+ setVisitorId('');
11
+ });
12
+
13
+ it('generates a valid UUIDv4 string', () => {
14
+ const uuid = generateUUID();
15
+ assert.equal(typeof uuid, 'string');
16
+ assert.match(uuid, /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i);
17
+ });
18
+
19
+ it('generates and persists visitorId', () => {
20
+ const id1 = getVisitorId();
21
+ assert.ok(id1);
22
+ assert.equal(typeof id1, 'string');
23
+
24
+ // Calling again returns the exact same visitorId
25
+ const id2 = getVisitorId();
26
+ assert.equal(id1, id2);
27
+ });
28
+
29
+ it('allows explicit override with setVisitorId', () => {
30
+ const customId = 'custom-visitor-12345';
31
+ setVisitorId(customId);
32
+ assert.equal(getVisitorId(), customId);
33
+ assert.equal(safeLocalStorage.getItem(VISITOR_ID_KEY), customId);
34
+ });
35
+ });