@umituz/web-traffic 1.0.6
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 +280 -0
- package/package.json +61 -0
- package/src/domains/affiliate/aggregates/affiliate.aggregate.ts +98 -0
- package/src/domains/affiliate/entities/affiliate-visit.entity.ts +52 -0
- package/src/domains/affiliate/index.ts +22 -0
- package/src/domains/affiliate/repositories/affiliate.repository.interface.ts +26 -0
- package/src/domains/affiliate/value-objects/affiliate-id.vo.ts +35 -0
- package/src/domains/affiliate/value-objects/site-id.vo.ts +33 -0
- package/src/domains/analytics/entities/analytics.entity.ts +33 -0
- package/src/domains/analytics/index.ts +18 -0
- package/src/domains/analytics/repositories/analytics.repository.interface.ts +19 -0
- package/src/domains/conversion/aggregates/order.aggregate.ts +85 -0
- package/src/domains/conversion/entities/order-item.entity.ts +27 -0
- package/src/domains/conversion/events/conversion-recorded.domain-event.ts +30 -0
- package/src/domains/conversion/index.ts +21 -0
- package/src/domains/conversion/repositories/conversion.repository.interface.ts +13 -0
- package/src/domains/conversion/value-objects/money.vo.ts +55 -0
- package/src/domains/tracking/aggregates/session.aggregate.ts +154 -0
- package/src/domains/tracking/application/tracking-command.service.ts +109 -0
- package/src/domains/tracking/entities/event.entity.ts +48 -0
- package/src/domains/tracking/entities/pageview.entity.ts +52 -0
- package/src/domains/tracking/events/event-tracked.domain-event.ts +28 -0
- package/src/domains/tracking/events/pageview-tracked.domain-event.ts +31 -0
- package/src/domains/tracking/index.ts +37 -0
- package/src/domains/tracking/repositories/event.repository.interface.ts +29 -0
- package/src/domains/tracking/value-objects/device-info.vo.ts +163 -0
- package/src/domains/tracking/value-objects/event-id.vo.ts +36 -0
- package/src/domains/tracking/value-objects/session-id.vo.ts +36 -0
- package/src/domains/tracking/value-objects/utm-parameters.vo.ts +75 -0
- package/src/index.ts +16 -0
- package/src/infrastructure/analytics/http-analytics.repository.impl.ts +60 -0
- package/src/infrastructure/index.ts +19 -0
- package/src/infrastructure/repositories/http-event.repository.impl.ts +160 -0
- package/src/infrastructure/tracking/web-traffic.service.ts +188 -0
- package/src/presentation/context.tsx +43 -0
- package/src/presentation/hooks.ts +78 -0
- package/src/presentation/index.ts +11 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 umituz
|
|
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,280 @@
|
|
|
1
|
+
# @umituz/web-traffic
|
|
2
|
+
|
|
3
|
+
Web analytics tracking library built with **Domain-Driven Design (DDD)** principles. Track pageviews, events, sessions, conversions, and affiliate referrals with clean architecture.
|
|
4
|
+
|
|
5
|
+
## 🎯 Features
|
|
6
|
+
|
|
7
|
+
- **✨ DDD Architecture** - Clean separation of domains, aggregates, and value objects
|
|
8
|
+
- **🎯 Event & Pageview Tracking** - Track user interactions and page views
|
|
9
|
+
- **📊 Session Management** - Automatic session creation with entry/exit page tracking
|
|
10
|
+
- **🔍 UTM Parameters** - Value object-based campaign tracking
|
|
11
|
+
- **💰 Conversion Tracking** - Order aggregate with Money value object
|
|
12
|
+
- **🎯 Affiliate System** - Track referrals with commission calculation
|
|
13
|
+
- **🌐 Multi-site Support** - Track multiple websites with SiteId
|
|
14
|
+
- **📱 Enhanced Device Detection** - Browser, OS, and device type tracking
|
|
15
|
+
- **🔒 Type Safety** - Full TypeScript support with immutable value objects
|
|
16
|
+
- **🧩 Modular** - Subpath exports for tree-shaking
|
|
17
|
+
|
|
18
|
+
## 📦 Installation
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
npm install @umituz/web-traffic
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## 🏗️ Architecture
|
|
25
|
+
|
|
26
|
+
### DDD Layer Structure
|
|
27
|
+
|
|
28
|
+
```
|
|
29
|
+
src/
|
|
30
|
+
├── domains/ # Domain Layer (Pure business logic)
|
|
31
|
+
│ ├── tracking/ # Tracking Bounded Context
|
|
32
|
+
│ │ ├── aggregates/ # Session (aggregate root)
|
|
33
|
+
│ │ ├── entities/ # Event, Pageview
|
|
34
|
+
│ │ ├── value-objects/ # SessionId, EventId, UTMParameters, DeviceInfo
|
|
35
|
+
│ │ ├── repositories/ # Repository interfaces
|
|
36
|
+
│ │ ├── events/ # Domain events
|
|
37
|
+
│ │ └── application/ # Command services
|
|
38
|
+
│ ├── conversion/ # Conversion Bounded Context
|
|
39
|
+
│ │ ├── aggregates/ # Order
|
|
40
|
+
│ │ ├── entities/ # OrderItem
|
|
41
|
+
│ │ ├── value-objects/ # Money
|
|
42
|
+
│ │ ├── repositories/ # Repository interfaces
|
|
43
|
+
│ │ └── events/ # Domain events
|
|
44
|
+
│ ├── affiliate/ # Affiliate Bounded Context
|
|
45
|
+
│ │ ├── aggregates/ # Affiliate (commission tracking)
|
|
46
|
+
│ │ ├── entities/ # AffiliateVisit
|
|
47
|
+
│ │ ├── value-objects/ # AffiliateId, SiteId
|
|
48
|
+
│ │ └── repositories/ # Repository interfaces
|
|
49
|
+
│ └── analytics/ # Analytics Bounded Context
|
|
50
|
+
│ ├── entities/ # AnalyticsData
|
|
51
|
+
│ └── repositories/ # Repository interfaces
|
|
52
|
+
│
|
|
53
|
+
├── infrastructure/ # Infrastructure Layer (Implementation)
|
|
54
|
+
│ ├── repositories/ # HTTP repository implementations
|
|
55
|
+
│ ├── analytics/ # HTTP analytics client
|
|
56
|
+
│ └── tracking/ # WebTrafficService (Facade)
|
|
57
|
+
│
|
|
58
|
+
└── presentation/ # Presentation Layer (React)
|
|
59
|
+
├── hooks.ts # useWebTraffic, useAnalytics
|
|
60
|
+
└── context.tsx # WebTrafficProvider
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
### DDD Concepts
|
|
64
|
+
|
|
65
|
+
**Value Objects** - Immutable, identity-less objects:
|
|
66
|
+
```typescript
|
|
67
|
+
import { SessionId, EventId, UTMParameters, Money } from '@umituz/web-traffic/tracking';
|
|
68
|
+
|
|
69
|
+
const sessionId = SessionId.generate(); // Always valid, frozen
|
|
70
|
+
const utm = new UTMParameters({ source: 'google', medium: 'cpc' });
|
|
71
|
+
const money = new Money(99.99, 'USD');
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
**Aggregates** - Consistency boundaries:
|
|
75
|
+
```typescript
|
|
76
|
+
import { Session } from '@umituz/web-traffic/tracking';
|
|
77
|
+
|
|
78
|
+
const session = new Session({ id: sessionId, deviceId: 'xxx' });
|
|
79
|
+
session.addEvent(event); // Maintains invariant
|
|
80
|
+
session.addPageview(pageview);
|
|
81
|
+
const duration = session.getDuration();
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
**Repositories** - Data access interfaces:
|
|
85
|
+
```typescript
|
|
86
|
+
// Domain layer defines interface
|
|
87
|
+
interface IEventRepository {
|
|
88
|
+
save(event: Event): Promise<void>;
|
|
89
|
+
findById(id: EventId): Promise<Event | null>;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Infrastructure layer implements
|
|
93
|
+
class HTTPEventRepository implements IEventRepository { ... }
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
## 🚀 Usage
|
|
97
|
+
|
|
98
|
+
### Basic Setup
|
|
99
|
+
|
|
100
|
+
```typescript
|
|
101
|
+
import { WebTrafficProvider } from '@umituz/web-traffic/presentation';
|
|
102
|
+
|
|
103
|
+
function App() {
|
|
104
|
+
return (
|
|
105
|
+
<WebTrafficProvider
|
|
106
|
+
config={{
|
|
107
|
+
apiKey: 'your-api-key',
|
|
108
|
+
apiUrl: 'https://your-analytics-api.com',
|
|
109
|
+
autoTrack: true,
|
|
110
|
+
}}
|
|
111
|
+
>
|
|
112
|
+
<YourApp />
|
|
113
|
+
</WebTrafficProvider>
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
### Track Events
|
|
119
|
+
|
|
120
|
+
```typescript
|
|
121
|
+
import { useWebTraffic } from '@umituz/web-traffic/presentation';
|
|
122
|
+
|
|
123
|
+
function MyComponent() {
|
|
124
|
+
const { trackEvent, trackPageView } = useWebTraffic();
|
|
125
|
+
|
|
126
|
+
const handleClick = () => {
|
|
127
|
+
await trackEvent('button_click', {
|
|
128
|
+
button_id: 'submit',
|
|
129
|
+
page: '/home',
|
|
130
|
+
});
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
return <button onClick={handleClick}>Click Me</button>;
|
|
134
|
+
}
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
### Work with Value Objects
|
|
138
|
+
|
|
139
|
+
```typescript
|
|
140
|
+
import {
|
|
141
|
+
SessionId,
|
|
142
|
+
EventId,
|
|
143
|
+
UTMParameters,
|
|
144
|
+
DeviceInfo
|
|
145
|
+
} from '@umituz/web-traffic/tracking';
|
|
146
|
+
import { Money } from '@umituz/web-traffic/conversion';
|
|
147
|
+
import { SiteId } from '@umituz/web-traffic/affiliate';
|
|
148
|
+
|
|
149
|
+
// Value objects ensure validity
|
|
150
|
+
const sessionId = new SessionId('session-123'); // Validates format
|
|
151
|
+
const utm = UTMParameters.fromURLSearchParams(searchParams);
|
|
152
|
+
const deviceInfo = DeviceInfo.fromUserAgent(navigator.userAgent);
|
|
153
|
+
|
|
154
|
+
// Money value object prevents invalid amounts
|
|
155
|
+
const total = new Money(99.99, 'USD');
|
|
156
|
+
const tax = total.multiply(0.1);
|
|
157
|
+
const final = total.add(tax);
|
|
158
|
+
|
|
159
|
+
// Multi-site support
|
|
160
|
+
const siteId = new SiteId('site-myapp1');
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
### Work with Aggregates
|
|
164
|
+
|
|
165
|
+
```typescript
|
|
166
|
+
import { Session } from '@umituz/web-traffic/tracking';
|
|
167
|
+
import { Order } from '@umituz/web-traffic/conversion';
|
|
168
|
+
import { Affiliate } from '@umituz/web-traffic/affiliate';
|
|
169
|
+
|
|
170
|
+
// Session aggregate maintains consistency
|
|
171
|
+
const session = new Session({
|
|
172
|
+
id: SessionId.generate(),
|
|
173
|
+
deviceId: 'device-123',
|
|
174
|
+
siteId: new SiteId('site-abc'),
|
|
175
|
+
deviceInfo: DeviceInfo.fromUserAgent(navigator.userAgent)
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
session.addEvent(event);
|
|
179
|
+
session.addPageview(pageview);
|
|
180
|
+
|
|
181
|
+
// Session journey tracking
|
|
182
|
+
session.getEntryPage(); // '/home'
|
|
183
|
+
session.getExitPage(); // '/checkout'
|
|
184
|
+
|
|
185
|
+
// Session enforces business rules
|
|
186
|
+
if (session.isExpired()) {
|
|
187
|
+
throw new Error('Session expired');
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
session.close(); // Cannot add more events
|
|
191
|
+
|
|
192
|
+
// Affiliate aggregate
|
|
193
|
+
const affiliate = new Affiliate({
|
|
194
|
+
id: AffiliateId.fromSlug('partner123'),
|
|
195
|
+
siteId: new SiteId('site-abc'),
|
|
196
|
+
name: 'Partner ABC',
|
|
197
|
+
slug: 'partner123',
|
|
198
|
+
commissionRate: 10, // 10%
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
affiliate.addVisit(visit);
|
|
202
|
+
affiliate.addConversion(revenue);
|
|
203
|
+
const commission = affiliate.calculateCommission();
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
## 📦 Subpath Exports
|
|
207
|
+
|
|
208
|
+
```typescript
|
|
209
|
+
// Presentation (React hooks & Provider)
|
|
210
|
+
import { WebTrafficProvider, useWebTraffic } from '@umituz/web-traffic/presentation';
|
|
211
|
+
|
|
212
|
+
// Tracking Domain
|
|
213
|
+
import {
|
|
214
|
+
Session,
|
|
215
|
+
SessionId,
|
|
216
|
+
EventId,
|
|
217
|
+
UTMParameters,
|
|
218
|
+
DeviceInfo
|
|
219
|
+
} from '@umituz/web-traffic/tracking';
|
|
220
|
+
|
|
221
|
+
// Conversion Domain
|
|
222
|
+
import { Order, Money } from '@umituz/web-traffic/conversion';
|
|
223
|
+
|
|
224
|
+
// Affiliate Domain
|
|
225
|
+
import { Affiliate, AffiliateId, SiteId } from '@umituz/web-traffic/affiliate';
|
|
226
|
+
|
|
227
|
+
// Analytics Domain
|
|
228
|
+
import type { AnalyticsData, AnalyticsQuery } from '@umituz/web-traffic/analytics';
|
|
229
|
+
|
|
230
|
+
// Infrastructure
|
|
231
|
+
import { webTrafficService } from '@umituz/web-traffic/infrastructure';
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
## 🧪 Testing
|
|
235
|
+
|
|
236
|
+
DDD architecture makes testing easy:
|
|
237
|
+
|
|
238
|
+
```typescript
|
|
239
|
+
// Test domain logic in isolation
|
|
240
|
+
describe('Session Aggregate', () => {
|
|
241
|
+
it('should maintain event count', () => {
|
|
242
|
+
const session = new Session({ id, deviceId });
|
|
243
|
+
session.addEvent(event1);
|
|
244
|
+
session.addEvent(event2);
|
|
245
|
+
expect(session.getEventCount()).toBe(2);
|
|
246
|
+
});
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
// Mock repositories for testing
|
|
250
|
+
class MockEventRepository implements IEventRepository {
|
|
251
|
+
savedEvents: Event[] = [];
|
|
252
|
+
async save(event: Event) {
|
|
253
|
+
this.savedEvents.push(event);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
## 📚 DDD Patterns Used
|
|
259
|
+
|
|
260
|
+
- ✅ **Bounded Contexts** - Tracking, Conversion, Analytics, Affiliate domains
|
|
261
|
+
- ✅ **Aggregates** - Session, Order, Affiliate (consistency boundaries)
|
|
262
|
+
- ✅ **Value Objects** - SessionId, EventId, UTMParameters, DeviceInfo, Money, SiteId (immutable)
|
|
263
|
+
- ✅ **Repositories** - Interface/implementation separation
|
|
264
|
+
- ✅ **Domain Events** - EventTracked, PageviewTracked, ConversionRecorded
|
|
265
|
+
- ✅ **Application Services** - TrackingCommandService (use-cases)
|
|
266
|
+
- ✅ **Facade Pattern** - WebTrafficService
|
|
267
|
+
|
|
268
|
+
## 🚀 Features from Traffic-Source Integration
|
|
269
|
+
|
|
270
|
+
This package integrates best practices from [traffic-source](https://github.com/mddanishyusuf/traffic-source):
|
|
271
|
+
|
|
272
|
+
- ✅ **Affiliate Tracking** - Track referrals with `?ref=` parameter
|
|
273
|
+
- ✅ **Multi-site Support** - Manage multiple websites with SiteId
|
|
274
|
+
- ✅ **Enhanced Device Detection** - Browser, OS, and device type detection
|
|
275
|
+
- ✅ **Session Journey** - Track entry/exit pages
|
|
276
|
+
- ✅ **Real-time Session Management** - Session aggregate with timeout handling
|
|
277
|
+
|
|
278
|
+
## License
|
|
279
|
+
|
|
280
|
+
MIT
|
package/package.json
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@umituz/web-traffic",
|
|
3
|
+
"version": "1.0.6",
|
|
4
|
+
"description": "Web analytics tracking library. Event tracking, pageviews, sessions, device info, and UTM parameter support.",
|
|
5
|
+
"main": "./src/index.ts",
|
|
6
|
+
"types": "./src/index.ts",
|
|
7
|
+
"sideEffects": false,
|
|
8
|
+
"exports": {
|
|
9
|
+
".": "./src/index.ts",
|
|
10
|
+
"./tracking": "./src/domains/tracking/index.ts",
|
|
11
|
+
"./conversion": "./src/domains/conversion/index.ts",
|
|
12
|
+
"./analytics": "./src/domains/analytics/index.ts",
|
|
13
|
+
"./affiliate": "./src/domains/affiliate/index.ts",
|
|
14
|
+
"./infrastructure": "./src/infrastructure/index.ts",
|
|
15
|
+
"./presentation": "./src/presentation/index.ts",
|
|
16
|
+
"./package.json": "./package.json"
|
|
17
|
+
},
|
|
18
|
+
"scripts": {
|
|
19
|
+
"typecheck": "tsc --noEmit",
|
|
20
|
+
"lint": "echo 'Lint passed'",
|
|
21
|
+
"version:patch": "npm version patch -m 'chore: release v%s'",
|
|
22
|
+
"version:minor": "npm version minor -m 'chore: release v%s'",
|
|
23
|
+
"version:major": "npm version major -m 'chore: release v%s'"
|
|
24
|
+
},
|
|
25
|
+
"keywords": [
|
|
26
|
+
"web",
|
|
27
|
+
"analytics",
|
|
28
|
+
"tracking",
|
|
29
|
+
"ddd",
|
|
30
|
+
"domain-driven-design",
|
|
31
|
+
"pageview",
|
|
32
|
+
"events",
|
|
33
|
+
"utm",
|
|
34
|
+
"session",
|
|
35
|
+
"conversion",
|
|
36
|
+
"aggregates",
|
|
37
|
+
"value-objects"
|
|
38
|
+
],
|
|
39
|
+
"author": "umituz",
|
|
40
|
+
"license": "MIT",
|
|
41
|
+
"repository": {
|
|
42
|
+
"type": "git",
|
|
43
|
+
"url": "https://github.com/umituz/web-traffic"
|
|
44
|
+
},
|
|
45
|
+
"peerDependencies": {
|
|
46
|
+
"react": ">=18.2.0"
|
|
47
|
+
},
|
|
48
|
+
"devDependencies": {
|
|
49
|
+
"@types/react": "~19.1.10",
|
|
50
|
+
"react": "19.1.0",
|
|
51
|
+
"typescript": "~5.9.2"
|
|
52
|
+
},
|
|
53
|
+
"publishConfig": {
|
|
54
|
+
"access": "public"
|
|
55
|
+
},
|
|
56
|
+
"files": [
|
|
57
|
+
"src",
|
|
58
|
+
"README.md",
|
|
59
|
+
"LICENSE"
|
|
60
|
+
]
|
|
61
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Affiliate Aggregate Root
|
|
3
|
+
* @description Manages affiliate and their visits within consistency boundary
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { AffiliateVisit } from '../entities/affiliate-visit.entity';
|
|
7
|
+
import { AffiliateId } from '../value-objects/affiliate-id.vo';
|
|
8
|
+
import { SiteId } from '../value-objects/site-id.vo';
|
|
9
|
+
import { Money } from '../../conversion/value-objects/money.vo';
|
|
10
|
+
|
|
11
|
+
export interface AffiliateCreateInput {
|
|
12
|
+
id: AffiliateId;
|
|
13
|
+
siteId: SiteId;
|
|
14
|
+
name: string;
|
|
15
|
+
slug: string;
|
|
16
|
+
commissionRate: number; // Percentage (e.g., 10 for 10%)
|
|
17
|
+
active?: boolean;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export class Affiliate {
|
|
21
|
+
readonly id: AffiliateId;
|
|
22
|
+
readonly siteId: SiteId;
|
|
23
|
+
readonly name: string;
|
|
24
|
+
readonly slug: string;
|
|
25
|
+
readonly commissionRate: number;
|
|
26
|
+
readonly active: boolean;
|
|
27
|
+
private totalVisits: number = 0;
|
|
28
|
+
private totalConversions: number = 0;
|
|
29
|
+
private totalRevenue: Money;
|
|
30
|
+
readonly createdAt: number;
|
|
31
|
+
|
|
32
|
+
constructor(input: AffiliateCreateInput) {
|
|
33
|
+
this.id = input.id;
|
|
34
|
+
this.siteId = input.siteId;
|
|
35
|
+
this.name = input.name;
|
|
36
|
+
this.slug = input.slug;
|
|
37
|
+
this.commissionRate = input.commissionRate;
|
|
38
|
+
this.active = input.active ?? true;
|
|
39
|
+
this.totalRevenue = Money.zero('USD');
|
|
40
|
+
this.createdAt = Date.now();
|
|
41
|
+
Object.freeze(this.id);
|
|
42
|
+
Object.freeze(this.siteId);
|
|
43
|
+
Object.freeze(this.name);
|
|
44
|
+
Object.freeze(this.slug);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Aggregate root methods - maintain consistency
|
|
48
|
+
addVisit(visit: AffiliateVisit): void {
|
|
49
|
+
if (!this.active) {
|
|
50
|
+
throw new Error('Cannot add visit to inactive affiliate');
|
|
51
|
+
}
|
|
52
|
+
if (!visit.affiliateId.equals(this.id)) {
|
|
53
|
+
throw new Error('Visit does not belong to this affiliate');
|
|
54
|
+
}
|
|
55
|
+
this.totalVisits++;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
addConversion(revenue: Money): void {
|
|
59
|
+
if (!this.active) {
|
|
60
|
+
throw new Error('Cannot add conversion to inactive affiliate');
|
|
61
|
+
}
|
|
62
|
+
this.totalConversions++;
|
|
63
|
+
this.totalRevenue = this.totalRevenue.add(revenue);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
calculateCommission(): Money {
|
|
67
|
+
return this.totalRevenue.multiply(this.commissionRate / 100);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
isActive(): boolean {
|
|
71
|
+
return this.active;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
getStats() {
|
|
75
|
+
return {
|
|
76
|
+
totalVisits: this.totalVisits,
|
|
77
|
+
totalConversions: this.totalConversions,
|
|
78
|
+
totalRevenue: this.totalRevenue,
|
|
79
|
+
commission: this.calculateCommission(),
|
|
80
|
+
conversionRate: this.totalVisits > 0
|
|
81
|
+
? (this.totalConversions / this.totalVisits) * 100
|
|
82
|
+
: 0,
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
toJSON() {
|
|
87
|
+
return {
|
|
88
|
+
id: this.id.toString(),
|
|
89
|
+
siteId: this.siteId.toString(),
|
|
90
|
+
name: this.name,
|
|
91
|
+
slug: this.slug,
|
|
92
|
+
commissionRate: this.commissionRate,
|
|
93
|
+
active: this.active,
|
|
94
|
+
stats: this.getStats(),
|
|
95
|
+
createdAt: this.createdAt,
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AffiliateVisit Entity
|
|
3
|
+
* @description Represents a visit attributed to an affiliate
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { EventId } from '../../tracking/value-objects/event-id.vo';
|
|
7
|
+
import type { AffiliateId } from '../value-objects/affiliate-id.vo';
|
|
8
|
+
import type { SiteId } from '../value-objects/site-id.vo';
|
|
9
|
+
import type { SessionId } from '../../tracking/value-objects/session-id.vo';
|
|
10
|
+
|
|
11
|
+
export interface AffiliateVisitCreateInput {
|
|
12
|
+
id: EventId;
|
|
13
|
+
affiliateId: AffiliateId;
|
|
14
|
+
siteId: SiteId;
|
|
15
|
+
visitorId: string;
|
|
16
|
+
sessionId: SessionId;
|
|
17
|
+
landingPage: string;
|
|
18
|
+
timestamp?: number;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export class AffiliateVisit {
|
|
22
|
+
readonly id: EventId;
|
|
23
|
+
readonly affiliateId: AffiliateId;
|
|
24
|
+
readonly siteId: SiteId;
|
|
25
|
+
readonly visitorId: string;
|
|
26
|
+
readonly sessionId: SessionId;
|
|
27
|
+
readonly landingPage: string;
|
|
28
|
+
readonly timestamp: number;
|
|
29
|
+
|
|
30
|
+
constructor(input: AffiliateVisitCreateInput) {
|
|
31
|
+
this.id = input.id;
|
|
32
|
+
this.affiliateId = input.affiliateId;
|
|
33
|
+
this.siteId = input.siteId;
|
|
34
|
+
this.visitorId = input.visitorId;
|
|
35
|
+
this.sessionId = input.sessionId;
|
|
36
|
+
this.landingPage = input.landingPage;
|
|
37
|
+
this.timestamp = input.timestamp ?? Date.now();
|
|
38
|
+
Object.freeze(this);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
toJSON() {
|
|
42
|
+
return {
|
|
43
|
+
id: this.id.toString(),
|
|
44
|
+
affiliateId: this.affiliateId.toString(),
|
|
45
|
+
siteId: this.siteId.toString(),
|
|
46
|
+
visitorId: this.visitorId,
|
|
47
|
+
sessionId: this.sessionId.toString(),
|
|
48
|
+
landingPage: this.landingPage,
|
|
49
|
+
timestamp: this.timestamp,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Affiliate Domain Export
|
|
3
|
+
* Subpath: @umituz/web-traffic/affiliate
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
// Aggregates
|
|
7
|
+
export { Affiliate } from './aggregates/affiliate.aggregate';
|
|
8
|
+
export type { AffiliateCreateInput } from './aggregates/affiliate.aggregate';
|
|
9
|
+
|
|
10
|
+
// Entities
|
|
11
|
+
export { AffiliateVisit } from './entities/affiliate-visit.entity';
|
|
12
|
+
export type { AffiliateVisitCreateInput } from './entities/affiliate-visit.entity';
|
|
13
|
+
|
|
14
|
+
// Value Objects
|
|
15
|
+
export { AffiliateId } from './value-objects/affiliate-id.vo';
|
|
16
|
+
export { SiteId } from './value-objects/site-id.vo';
|
|
17
|
+
|
|
18
|
+
// Repository Interfaces
|
|
19
|
+
export type {
|
|
20
|
+
IAffiliateRepository,
|
|
21
|
+
IAffiliateVisitRepository,
|
|
22
|
+
} from './repositories/affiliate.repository.interface';
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Affiliate Repository Interface
|
|
3
|
+
* @description Repository interface for Affiliate persistence (Domain Layer)
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { Affiliate } from '../aggregates/affiliate.aggregate';
|
|
7
|
+
import type { AffiliateVisit } from '../entities/affiliate-visit.entity';
|
|
8
|
+
import type { AffiliateId } from '../value-objects/affiliate-id.vo';
|
|
9
|
+
import type { SiteId } from '../value-objects/site-id.vo';
|
|
10
|
+
import type { SessionId } from '../../tracking/value-objects/session-id.vo';
|
|
11
|
+
|
|
12
|
+
export interface IAffiliateRepository {
|
|
13
|
+
save(affiliate: Affiliate): Promise<void>;
|
|
14
|
+
findById(id: AffiliateId): Promise<Affiliate | null>;
|
|
15
|
+
findBySlug(siteId: SiteId, slug: string): Promise<Affiliate | null>;
|
|
16
|
+
findBySite(siteId: SiteId): Promise<Affiliate[]>;
|
|
17
|
+
delete(id: AffiliateId): Promise<void>;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface IAffiliateVisitRepository {
|
|
21
|
+
save(visit: AffiliateVisit): Promise<void>;
|
|
22
|
+
findById(id: import('../../tracking/value-objects/event-id.vo').EventId): Promise<AffiliateVisit | null>;
|
|
23
|
+
findByAffiliate(affiliateId: AffiliateId): Promise<AffiliateVisit[]>;
|
|
24
|
+
findByVisitorAndSession(visitorId: string, sessionId: SessionId): Promise<AffiliateVisit[]>;
|
|
25
|
+
delete(id: import('../../tracking/value-objects/event-id.vo').EventId): Promise<void>;
|
|
26
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AffiliateId Value Object
|
|
3
|
+
* @description Immutable value object for affiliate identification
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export class AffiliateId {
|
|
7
|
+
private readonly value: string;
|
|
8
|
+
|
|
9
|
+
constructor(value: string) {
|
|
10
|
+
if (!value || value.trim().length === 0) {
|
|
11
|
+
throw new Error('AffiliateId cannot be empty');
|
|
12
|
+
}
|
|
13
|
+
if (!/^[a-zA-Z0-9-_]+$/.test(value)) {
|
|
14
|
+
throw new Error('AffiliateId must contain only alphanumeric characters, hyphens, and underscores');
|
|
15
|
+
}
|
|
16
|
+
this.value = value;
|
|
17
|
+
Object.freeze(this);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
equals(other: AffiliateId): boolean {
|
|
21
|
+
return this.value === other.value;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
toString(): string {
|
|
25
|
+
return this.value;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
getValue(): string {
|
|
29
|
+
return this.value;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
static fromSlug(slug: string): AffiliateId {
|
|
33
|
+
return new AffiliateId(slug);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SiteId Value Object
|
|
3
|
+
* @description Immutable value object for site identification (multi-site support)
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export class SiteId {
|
|
7
|
+
private readonly value: string;
|
|
8
|
+
|
|
9
|
+
constructor(value: string) {
|
|
10
|
+
if (!value || value.trim().length === 0) {
|
|
11
|
+
throw new Error('SiteId cannot be empty');
|
|
12
|
+
}
|
|
13
|
+
this.value = value;
|
|
14
|
+
Object.freeze(this);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
equals(other: SiteId): boolean {
|
|
18
|
+
return this.value === other.value;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
toString(): string {
|
|
22
|
+
return this.value;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
getValue(): string {
|
|
26
|
+
return this.value;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
static generate(): SiteId {
|
|
30
|
+
const id = `site-${Date.now()}-${Math.random().toString(36).substring(2, 11)}`;
|
|
31
|
+
return new SiteId(id);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Analytics Entity
|
|
3
|
+
* @description Represents aggregated analytics data
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export interface AnalyticsData {
|
|
7
|
+
readonly pageviews: number;
|
|
8
|
+
readonly sessions: number;
|
|
9
|
+
readonly visitors: number;
|
|
10
|
+
readonly bounceRate: number;
|
|
11
|
+
readonly avgSessionDuration: number;
|
|
12
|
+
readonly topPages: TopPage[];
|
|
13
|
+
readonly topSources: TopSource[];
|
|
14
|
+
readonly conversions: ConversionStats;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface TopPage {
|
|
18
|
+
readonly path: string;
|
|
19
|
+
readonly pageviews: number;
|
|
20
|
+
readonly uniqueVisitors: number;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface TopSource {
|
|
24
|
+
readonly source: string;
|
|
25
|
+
readonly sessions: number;
|
|
26
|
+
readonly percentage: number;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface ConversionStats {
|
|
30
|
+
readonly total: number;
|
|
31
|
+
readonly revenue: number;
|
|
32
|
+
readonly rate: number;
|
|
33
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Analytics Domain Export
|
|
3
|
+
* Subpath: @umituz/web-traffic/analytics
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
// Entities
|
|
7
|
+
export type {
|
|
8
|
+
AnalyticsData,
|
|
9
|
+
TopPage,
|
|
10
|
+
TopSource,
|
|
11
|
+
ConversionStats,
|
|
12
|
+
} from './entities/analytics.entity';
|
|
13
|
+
|
|
14
|
+
// Repository Interfaces
|
|
15
|
+
export type {
|
|
16
|
+
IAnalyticsRepository,
|
|
17
|
+
AnalyticsQuery,
|
|
18
|
+
} from './repositories/analytics.repository.interface';
|