rename to core
This commit is contained in:
149
core/analytics/domain/entities/AnalyticsSnapshot.ts
Normal file
149
core/analytics/domain/entities/AnalyticsSnapshot.ts
Normal file
@@ -0,0 +1,149 @@
|
||||
/**
|
||||
* Domain Entity: AnalyticsSnapshot
|
||||
*
|
||||
* Aggregated analytics data for a specific entity over a time period.
|
||||
* Pre-calculated metrics for sponsor dashboard and entity analytics.
|
||||
*/
|
||||
|
||||
import type { IEntity } from '@gridpilot/shared/domain';
|
||||
import type {
|
||||
AnalyticsSnapshotProps,
|
||||
AnalyticsMetrics,
|
||||
SnapshotEntityType,
|
||||
SnapshotPeriod,
|
||||
} from '../types/AnalyticsSnapshot';
|
||||
export type { SnapshotEntityType, SnapshotPeriod } from '../types/AnalyticsSnapshot';
|
||||
import { AnalyticsEntityId } from '../value-objects/AnalyticsEntityId';
|
||||
|
||||
export class AnalyticsSnapshot implements IEntity<string> {
|
||||
readonly id: string;
|
||||
readonly entityType: SnapshotEntityType;
|
||||
readonly period: SnapshotPeriod;
|
||||
readonly startDate: Date;
|
||||
readonly endDate: Date;
|
||||
readonly metrics: AnalyticsMetrics;
|
||||
readonly createdAt: Date;
|
||||
|
||||
private readonly entityIdVo: AnalyticsEntityId;
|
||||
|
||||
private constructor(props: AnalyticsSnapshotProps) {
|
||||
this.id = props.id;
|
||||
this.entityType = props.entityType;
|
||||
this.entityIdVo = AnalyticsEntityId.create(props.entityId);
|
||||
this.period = props.period;
|
||||
this.startDate = props.startDate;
|
||||
this.endDate = props.endDate;
|
||||
this.metrics = props.metrics;
|
||||
this.createdAt = props.createdAt;
|
||||
}
|
||||
|
||||
get entityId(): string {
|
||||
return this.entityIdVo.value;
|
||||
}
|
||||
|
||||
static create(props: Omit<AnalyticsSnapshotProps, 'createdAt'> & { createdAt?: Date }): AnalyticsSnapshot {
|
||||
this.validate(props);
|
||||
|
||||
return new AnalyticsSnapshot({
|
||||
...props,
|
||||
createdAt: props.createdAt ?? new Date(),
|
||||
});
|
||||
}
|
||||
|
||||
static createEmpty(
|
||||
id: string,
|
||||
entityType: SnapshotEntityType,
|
||||
entityId: string,
|
||||
period: SnapshotPeriod,
|
||||
startDate: Date,
|
||||
endDate: Date
|
||||
): AnalyticsSnapshot {
|
||||
return new AnalyticsSnapshot({
|
||||
id,
|
||||
entityType,
|
||||
entityId,
|
||||
period,
|
||||
startDate,
|
||||
endDate,
|
||||
metrics: {
|
||||
pageViews: 0,
|
||||
uniqueVisitors: 0,
|
||||
avgSessionDuration: 0,
|
||||
bounceRate: 0,
|
||||
engagementScore: 0,
|
||||
sponsorClicks: 0,
|
||||
sponsorUrlClicks: 0,
|
||||
socialShares: 0,
|
||||
leagueJoins: 0,
|
||||
raceRegistrations: 0,
|
||||
exposureValue: 0,
|
||||
},
|
||||
createdAt: new Date(),
|
||||
});
|
||||
}
|
||||
|
||||
private static validate(props: Omit<AnalyticsSnapshotProps, 'createdAt'>): void {
|
||||
if (!props.id || props.id.trim().length === 0) {
|
||||
throw new Error('AnalyticsSnapshot ID is required');
|
||||
}
|
||||
|
||||
if (!props.entityType) {
|
||||
throw new Error('AnalyticsSnapshot entityType is required');
|
||||
}
|
||||
|
||||
if (!props.entityId || props.entityId.trim().length === 0) {
|
||||
throw new Error('AnalyticsSnapshot entityId is required');
|
||||
}
|
||||
|
||||
if (!props.period) {
|
||||
throw new Error('AnalyticsSnapshot period is required');
|
||||
}
|
||||
|
||||
if (props.endDate < props.startDate) {
|
||||
throw new Error('AnalyticsSnapshot endDate must be after startDate');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate exposure score for sponsors (weighted combination of metrics)
|
||||
*/
|
||||
calculateExposureScore(): number {
|
||||
const { pageViews, uniqueVisitors, sponsorClicks, sponsorUrlClicks, socialShares } = this.metrics;
|
||||
|
||||
return (
|
||||
pageViews * 1 +
|
||||
uniqueVisitors * 2 +
|
||||
sponsorClicks * 10 +
|
||||
sponsorUrlClicks * 25 +
|
||||
socialShares * 5
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate trust indicator based on engagement quality
|
||||
*/
|
||||
getTrustIndicator(): 'high' | 'medium' | 'low' {
|
||||
const { bounceRate, avgSessionDuration, engagementScore } = this.metrics;
|
||||
|
||||
if (bounceRate < 30 && avgSessionDuration > 120000 && engagementScore > 50) {
|
||||
return 'high';
|
||||
}
|
||||
if (bounceRate < 60 && avgSessionDuration > 30000 && engagementScore > 20) {
|
||||
return 'medium';
|
||||
}
|
||||
return 'low';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get period comparison label
|
||||
*/
|
||||
getPeriodLabel(): string {
|
||||
const formatter = new Intl.DateTimeFormat('en-US', {
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
year: this.period === 'monthly' ? 'numeric' : undefined
|
||||
});
|
||||
|
||||
return `${formatter.format(this.startDate)} - ${formatter.format(this.endDate)}`;
|
||||
}
|
||||
}
|
||||
111
core/analytics/domain/entities/EngagementEvent.ts
Normal file
111
core/analytics/domain/entities/EngagementEvent.ts
Normal file
@@ -0,0 +1,111 @@
|
||||
/**
|
||||
* Domain Entity: EngagementEvent
|
||||
*
|
||||
* Represents user interactions beyond page views.
|
||||
* Tracks clicks, downloads, sign-ups, and other engagement actions.
|
||||
*/
|
||||
|
||||
import type { IEntity } from '@gridpilot/shared/domain';
|
||||
import type {
|
||||
EngagementAction,
|
||||
EngagementEntityType,
|
||||
EngagementEventProps,
|
||||
} from '../types/EngagementEvent';
|
||||
|
||||
export type { EngagementAction, EngagementEntityType } from '../types/EngagementEvent';
|
||||
import { AnalyticsEntityId } from '../value-objects/AnalyticsEntityId';
|
||||
|
||||
export class EngagementEvent implements IEntity<string> {
|
||||
readonly id: string;
|
||||
readonly action: EngagementAction;
|
||||
readonly entityType: EngagementEntityType;
|
||||
readonly actorId: string | undefined;
|
||||
readonly actorType: 'anonymous' | 'driver' | 'sponsor';
|
||||
readonly sessionId: string;
|
||||
readonly metadata: Record<string, string | number | boolean> | undefined;
|
||||
readonly timestamp: Date;
|
||||
|
||||
private readonly entityIdVo: AnalyticsEntityId;
|
||||
|
||||
private constructor(props: EngagementEventProps) {
|
||||
this.id = props.id;
|
||||
this.action = props.action;
|
||||
this.entityType = props.entityType;
|
||||
this.entityIdVo = AnalyticsEntityId.create(props.entityId);
|
||||
this.actorId = props.actorId;
|
||||
this.actorType = props.actorType;
|
||||
this.sessionId = props.sessionId;
|
||||
this.metadata = props.metadata;
|
||||
this.timestamp = props.timestamp;
|
||||
}
|
||||
|
||||
get entityId(): string {
|
||||
return this.entityIdVo.value;
|
||||
}
|
||||
|
||||
static create(props: Omit<EngagementEventProps, 'timestamp'> & { timestamp?: Date }): EngagementEvent {
|
||||
this.validate(props);
|
||||
|
||||
return new EngagementEvent({
|
||||
...props,
|
||||
timestamp: props.timestamp ?? new Date(),
|
||||
});
|
||||
}
|
||||
|
||||
private static validate(props: Omit<EngagementEventProps, 'timestamp'>): void {
|
||||
if (!props.id || props.id.trim().length === 0) {
|
||||
throw new Error('EngagementEvent ID is required');
|
||||
}
|
||||
|
||||
if (!props.action) {
|
||||
throw new Error('EngagementEvent action is required');
|
||||
}
|
||||
|
||||
if (!props.entityType) {
|
||||
throw new Error('EngagementEvent entityType is required');
|
||||
}
|
||||
|
||||
if (!props.entityId || props.entityId.trim().length === 0) {
|
||||
throw new Error('EngagementEvent entityId is required');
|
||||
}
|
||||
|
||||
if (!props.sessionId || props.sessionId.trim().length === 0) {
|
||||
throw new Error('EngagementEvent sessionId is required');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if this is a sponsor-related engagement
|
||||
*/
|
||||
isSponsorEngagement(): boolean {
|
||||
return this.action.startsWith('click_sponsor') ||
|
||||
this.action === 'contact_sponsor' ||
|
||||
this.entityType === 'sponsor' ||
|
||||
this.entityType === 'sponsorship';
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if this is a conversion event (high-value action)
|
||||
*/
|
||||
isConversionEvent(): boolean {
|
||||
return ['join_league', 'register_race', 'click_sponsor_url', 'contact_sponsor'].includes(this.action);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get engagement weight for analytics calculations
|
||||
*/
|
||||
getEngagementWeight(): number {
|
||||
const weights: Record<EngagementAction, number> = {
|
||||
'click_sponsor_logo': 2,
|
||||
'click_sponsor_url': 5,
|
||||
'download_livery_pack': 3,
|
||||
'join_league': 10,
|
||||
'register_race': 8,
|
||||
'view_standings': 1,
|
||||
'view_schedule': 1,
|
||||
'share_social': 4,
|
||||
'contact_sponsor': 15,
|
||||
};
|
||||
return weights[this.action] || 1;
|
||||
}
|
||||
}
|
||||
131
core/analytics/domain/entities/PageView.ts
Normal file
131
core/analytics/domain/entities/PageView.ts
Normal file
@@ -0,0 +1,131 @@
|
||||
/**
|
||||
* Domain Entity: PageView
|
||||
*
|
||||
* Represents a single page view event for analytics tracking.
|
||||
* Captures visitor interactions with leagues, drivers, teams, races.
|
||||
*/
|
||||
|
||||
import type { IEntity } from '@gridpilot/shared/domain';
|
||||
import type { EntityType, VisitorType, PageViewProps } from '../types/PageView';
|
||||
|
||||
export type { EntityType, VisitorType } from '../types/PageView';
|
||||
import { AnalyticsEntityId } from '../value-objects/AnalyticsEntityId';
|
||||
import { AnalyticsSessionId } from '../value-objects/AnalyticsSessionId';
|
||||
import { PageViewId } from '../value-objects/PageViewId';
|
||||
|
||||
export class PageView implements IEntity<string> {
|
||||
readonly entityType: EntityType;
|
||||
readonly visitorId: string | undefined;
|
||||
readonly visitorType: VisitorType;
|
||||
readonly referrer: string | undefined;
|
||||
readonly userAgent: string | undefined;
|
||||
readonly country: string | undefined;
|
||||
readonly timestamp: Date;
|
||||
readonly durationMs: number | undefined;
|
||||
|
||||
private readonly idVo: PageViewId;
|
||||
private readonly entityIdVo: AnalyticsEntityId;
|
||||
private readonly sessionIdVo: AnalyticsSessionId;
|
||||
|
||||
private constructor(props: PageViewProps) {
|
||||
this.idVo = PageViewId.create(props.id);
|
||||
this.entityType = props.entityType;
|
||||
this.entityIdVo = AnalyticsEntityId.create(props.entityId);
|
||||
this.visitorId = props.visitorId;
|
||||
this.visitorType = props.visitorType;
|
||||
this.sessionIdVo = AnalyticsSessionId.create(props.sessionId);
|
||||
this.referrer = props.referrer;
|
||||
this.userAgent = props.userAgent;
|
||||
this.country = props.country;
|
||||
this.timestamp = props.timestamp;
|
||||
this.durationMs = props.durationMs;
|
||||
}
|
||||
|
||||
get id(): string {
|
||||
return this.idVo.value;
|
||||
}
|
||||
|
||||
get entityId(): string {
|
||||
return this.entityIdVo.value;
|
||||
}
|
||||
|
||||
get sessionId(): string {
|
||||
return this.sessionIdVo.value;
|
||||
}
|
||||
|
||||
static create(props: Omit<PageViewProps, 'timestamp'> & { timestamp?: Date }): PageView {
|
||||
this.validate(props);
|
||||
|
||||
const baseProps: PageViewProps = {
|
||||
id: props.id,
|
||||
entityType: props.entityType,
|
||||
entityId: props.entityId,
|
||||
visitorType: props.visitorType,
|
||||
sessionId: props.sessionId,
|
||||
timestamp: props.timestamp ?? new Date(),
|
||||
...(props.visitorId !== undefined ? { visitorId: props.visitorId } : {}),
|
||||
...(props.referrer !== undefined ? { referrer: props.referrer } : {}),
|
||||
...(props.userAgent !== undefined ? { userAgent: props.userAgent } : {}),
|
||||
...(props.country !== undefined ? { country: props.country } : {}),
|
||||
...(props.durationMs !== undefined ? { durationMs: props.durationMs } : {}),
|
||||
};
|
||||
|
||||
return new PageView(baseProps);
|
||||
}
|
||||
|
||||
private static validate(props: Omit<PageViewProps, 'timestamp'>): void {
|
||||
if (!props.id || props.id.trim().length === 0) {
|
||||
throw new Error('PageView ID is required');
|
||||
}
|
||||
|
||||
if (!props.entityType) {
|
||||
throw new Error('PageView entityType is required');
|
||||
}
|
||||
|
||||
if (!props.entityId || props.entityId.trim().length === 0) {
|
||||
throw new Error('PageView entityId is required');
|
||||
}
|
||||
|
||||
if (!props.sessionId || props.sessionId.trim().length === 0) {
|
||||
throw new Error('PageView sessionId is required');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update duration when visitor leaves page
|
||||
*/
|
||||
withDuration(durationMs: number): PageView {
|
||||
if (durationMs < 0) {
|
||||
throw new Error('Duration must be non-negative');
|
||||
}
|
||||
|
||||
return PageView.create({
|
||||
id: this.id,
|
||||
entityType: this.entityType,
|
||||
entityId: this.entityId,
|
||||
visitorType: this.visitorType,
|
||||
sessionId: this.sessionId,
|
||||
timestamp: this.timestamp,
|
||||
...(this.visitorId !== undefined ? { visitorId: this.visitorId } : {}),
|
||||
...(this.referrer !== undefined ? { referrer: this.referrer } : {}),
|
||||
...(this.userAgent !== undefined ? { userAgent: this.userAgent } : {}),
|
||||
...(this.country !== undefined ? { country: this.country } : {}),
|
||||
durationMs,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if this is a meaningful view (not a bounce)
|
||||
*/
|
||||
isMeaningfulView(): boolean {
|
||||
return this.durationMs !== undefined && this.durationMs >= 5000; // 5+ seconds
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if view came from external source
|
||||
*/
|
||||
isExternalReferral(): boolean {
|
||||
if (!this.referrer) return false;
|
||||
return !this.referrer.includes('gridpilot');
|
||||
}
|
||||
}
|
||||
6
core/analytics/domain/ports/ILogger.ts
Normal file
6
core/analytics/domain/ports/ILogger.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
export interface ILogger {
|
||||
debug(message: string, ...args: any[]): void;
|
||||
info(message: string, ...args: any[]): void;
|
||||
warn(message: string, ...args: any[]): void;
|
||||
error(message: string, ...args: any[]): void;
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
/**
|
||||
* Repository Interface: IAnalyticsSnapshotRepository
|
||||
*
|
||||
* Defines persistence operations for AnalyticsSnapshot entities.
|
||||
*/
|
||||
|
||||
import type { AnalyticsSnapshot, SnapshotPeriod, SnapshotEntityType } from '../entities/AnalyticsSnapshot';
|
||||
|
||||
export interface IAnalyticsSnapshotRepository {
|
||||
save(snapshot: AnalyticsSnapshot): Promise<void>;
|
||||
findById(id: string): Promise<AnalyticsSnapshot | null>;
|
||||
findByEntity(entityType: SnapshotEntityType, entityId: string): Promise<AnalyticsSnapshot[]>;
|
||||
findByPeriod(
|
||||
entityType: SnapshotEntityType,
|
||||
entityId: string,
|
||||
period: SnapshotPeriod,
|
||||
startDate: Date,
|
||||
endDate: Date
|
||||
): Promise<AnalyticsSnapshot | null>;
|
||||
findLatest(entityType: SnapshotEntityType, entityId: string, period: SnapshotPeriod): Promise<AnalyticsSnapshot | null>;
|
||||
getHistoricalSnapshots(
|
||||
entityType: SnapshotEntityType,
|
||||
entityId: string,
|
||||
period: SnapshotPeriod,
|
||||
limit: number
|
||||
): Promise<AnalyticsSnapshot[]>;
|
||||
}
|
||||
17
core/analytics/domain/repositories/IEngagementRepository.ts
Normal file
17
core/analytics/domain/repositories/IEngagementRepository.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
/**
|
||||
* Repository Interface: IEngagementRepository
|
||||
*
|
||||
* Defines persistence operations for EngagementEvent entities.
|
||||
*/
|
||||
|
||||
import type { EngagementEvent, EngagementAction, EngagementEntityType } from '../entities/EngagementEvent';
|
||||
|
||||
export interface IEngagementRepository {
|
||||
save(event: EngagementEvent): Promise<void>;
|
||||
findById(id: string): Promise<EngagementEvent | null>;
|
||||
findByEntityId(entityType: EngagementEntityType, entityId: string): Promise<EngagementEvent[]>;
|
||||
findByAction(action: EngagementAction): Promise<EngagementEvent[]>;
|
||||
findByDateRange(startDate: Date, endDate: Date): Promise<EngagementEvent[]>;
|
||||
countByAction(action: EngagementAction, entityId?: string, since?: Date): Promise<number>;
|
||||
getSponsorClicksForEntity(entityId: string, since?: Date): Promise<number>;
|
||||
}
|
||||
17
core/analytics/domain/repositories/IPageViewRepository.ts
Normal file
17
core/analytics/domain/repositories/IPageViewRepository.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
/**
|
||||
* Repository Interface: IPageViewRepository
|
||||
*
|
||||
* Defines persistence operations for PageView entities.
|
||||
*/
|
||||
|
||||
import type { PageView, EntityType } from '../entities/PageView';
|
||||
|
||||
export interface IPageViewRepository {
|
||||
save(pageView: PageView): Promise<void>;
|
||||
findById(id: string): Promise<PageView | null>;
|
||||
findByEntityId(entityType: EntityType, entityId: string): Promise<PageView[]>;
|
||||
findByDateRange(startDate: Date, endDate: Date): Promise<PageView[]>;
|
||||
findBySession(sessionId: string): Promise<PageView[]>;
|
||||
countByEntityId(entityType: EntityType, entityId: string, since?: Date): Promise<number>;
|
||||
countUniqueVisitors(entityType: EntityType, entityId: string, since?: Date): Promise<number>;
|
||||
}
|
||||
35
core/analytics/domain/types/AnalyticsSnapshot.ts
Normal file
35
core/analytics/domain/types/AnalyticsSnapshot.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
/**
|
||||
* Domain Types: AnalyticsSnapshot
|
||||
*
|
||||
* Pure type/config definitions used by the AnalyticsSnapshot entity.
|
||||
* Kept in domain/types so domain/entities contains only entity classes.
|
||||
*/
|
||||
|
||||
export type SnapshotPeriod = 'daily' | 'weekly' | 'monthly';
|
||||
|
||||
export type SnapshotEntityType = 'league' | 'driver' | 'team' | 'race' | 'sponsor';
|
||||
|
||||
export interface AnalyticsMetrics {
|
||||
pageViews: number;
|
||||
uniqueVisitors: number;
|
||||
avgSessionDuration: number;
|
||||
bounceRate: number;
|
||||
engagementScore: number;
|
||||
sponsorClicks: number;
|
||||
sponsorUrlClicks: number;
|
||||
socialShares: number;
|
||||
leagueJoins: number;
|
||||
raceRegistrations: number;
|
||||
exposureValue: number;
|
||||
}
|
||||
|
||||
export interface AnalyticsSnapshotProps {
|
||||
id: string;
|
||||
entityType: SnapshotEntityType;
|
||||
entityId: string;
|
||||
period: SnapshotPeriod;
|
||||
startDate: Date;
|
||||
endDate: Date;
|
||||
metrics: AnalyticsMetrics;
|
||||
createdAt: Date;
|
||||
}
|
||||
37
core/analytics/domain/types/EngagementEvent.ts
Normal file
37
core/analytics/domain/types/EngagementEvent.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
/**
|
||||
* Domain Types: EngagementEvent
|
||||
*
|
||||
* Pure type/config definitions used by the EngagementEvent entity.
|
||||
* Kept in domain/types so domain/entities contains only entity classes.
|
||||
*/
|
||||
|
||||
export type EngagementAction =
|
||||
| 'click_sponsor_logo'
|
||||
| 'click_sponsor_url'
|
||||
| 'download_livery_pack'
|
||||
| 'join_league'
|
||||
| 'register_race'
|
||||
| 'view_standings'
|
||||
| 'view_schedule'
|
||||
| 'share_social'
|
||||
| 'contact_sponsor';
|
||||
|
||||
export type EngagementEntityType =
|
||||
| 'league'
|
||||
| 'driver'
|
||||
| 'team'
|
||||
| 'race'
|
||||
| 'sponsor'
|
||||
| 'sponsorship';
|
||||
|
||||
export interface EngagementEventProps {
|
||||
id: string;
|
||||
action: EngagementAction;
|
||||
entityType: EngagementEntityType;
|
||||
entityId: string;
|
||||
actorId?: string;
|
||||
actorType: 'anonymous' | 'driver' | 'sponsor';
|
||||
sessionId: string;
|
||||
metadata?: Record<string, string | number | boolean>;
|
||||
timestamp: Date;
|
||||
}
|
||||
34
core/analytics/domain/types/PageView.ts
Normal file
34
core/analytics/domain/types/PageView.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
/**
|
||||
* Domain Types: PageView
|
||||
*
|
||||
* Pure type/config definitions used by the PageView entity.
|
||||
* Kept in domain/types so domain/entities contains only entity classes.
|
||||
*/
|
||||
|
||||
export enum EntityType {
|
||||
LEAGUE = 'league',
|
||||
DRIVER = 'driver',
|
||||
TEAM = 'team',
|
||||
RACE = 'race',
|
||||
SPONSOR = 'sponsor',
|
||||
}
|
||||
|
||||
export enum VisitorType {
|
||||
ANONYMOUS = 'anonymous',
|
||||
DRIVER = 'driver',
|
||||
SPONSOR = 'sponsor',
|
||||
}
|
||||
|
||||
export interface PageViewProps {
|
||||
id: string;
|
||||
entityType: EntityType;
|
||||
entityId: string;
|
||||
visitorId?: string;
|
||||
visitorType: VisitorType;
|
||||
sessionId: string;
|
||||
referrer?: string;
|
||||
userAgent?: string;
|
||||
country?: string;
|
||||
timestamp: Date;
|
||||
durationMs?: number;
|
||||
}
|
||||
37
core/analytics/domain/value-objects/AnalyticsEntityId.ts
Normal file
37
core/analytics/domain/value-objects/AnalyticsEntityId.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import type { IValueObject } from '@gridpilot/shared/domain';
|
||||
|
||||
export interface AnalyticsEntityIdProps {
|
||||
value: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Value Object: AnalyticsEntityId
|
||||
*
|
||||
* Represents the ID of an entity (league, driver, team, race, sponsor)
|
||||
* within the analytics bounded context.
|
||||
*/
|
||||
export class AnalyticsEntityId implements IValueObject<AnalyticsEntityIdProps> {
|
||||
public readonly props: AnalyticsEntityIdProps;
|
||||
|
||||
private constructor(value: string) {
|
||||
this.props = { value };
|
||||
}
|
||||
|
||||
static create(raw: string): AnalyticsEntityId {
|
||||
const value = raw.trim();
|
||||
|
||||
if (!value) {
|
||||
throw new Error('AnalyticsEntityId must be a non-empty string');
|
||||
}
|
||||
|
||||
return new AnalyticsEntityId(value);
|
||||
}
|
||||
|
||||
get value(): string {
|
||||
return this.props.value;
|
||||
}
|
||||
|
||||
equals(other: IValueObject<AnalyticsEntityIdProps>): boolean {
|
||||
return this.props.value === other.props.value;
|
||||
}
|
||||
}
|
||||
36
core/analytics/domain/value-objects/AnalyticsSessionId.ts
Normal file
36
core/analytics/domain/value-objects/AnalyticsSessionId.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import type { IValueObject } from '@gridpilot/shared/domain';
|
||||
|
||||
export interface AnalyticsSessionIdProps {
|
||||
value: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Value Object: AnalyticsSessionId
|
||||
*
|
||||
* Represents an analytics session identifier within the analytics bounded context.
|
||||
*/
|
||||
export class AnalyticsSessionId implements IValueObject<AnalyticsSessionIdProps> {
|
||||
public readonly props: AnalyticsSessionIdProps;
|
||||
|
||||
private constructor(value: string) {
|
||||
this.props = { value };
|
||||
}
|
||||
|
||||
static create(raw: string): AnalyticsSessionId {
|
||||
const value = raw.trim();
|
||||
|
||||
if (!value) {
|
||||
throw new Error('AnalyticsSessionId must be a non-empty string');
|
||||
}
|
||||
|
||||
return new AnalyticsSessionId(value);
|
||||
}
|
||||
|
||||
get value(): string {
|
||||
return this.props.value;
|
||||
}
|
||||
|
||||
equals(other: IValueObject<AnalyticsSessionIdProps>): boolean {
|
||||
return this.props.value === other.props.value;
|
||||
}
|
||||
}
|
||||
36
core/analytics/domain/value-objects/PageViewId.ts
Normal file
36
core/analytics/domain/value-objects/PageViewId.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import type { IValueObject } from '@gridpilot/shared/domain';
|
||||
|
||||
export interface PageViewIdProps {
|
||||
value: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Value Object: PageViewId
|
||||
*
|
||||
* Represents the identifier of a PageView within the analytics bounded context.
|
||||
*/
|
||||
export class PageViewId implements IValueObject<PageViewIdProps> {
|
||||
public readonly props: PageViewIdProps;
|
||||
|
||||
private constructor(value: string) {
|
||||
this.props = { value };
|
||||
}
|
||||
|
||||
static create(raw: string): PageViewId {
|
||||
const value = raw.trim();
|
||||
|
||||
if (!value) {
|
||||
throw new Error('PageViewId must be a non-empty string');
|
||||
}
|
||||
|
||||
return new PageViewId(value);
|
||||
}
|
||||
|
||||
get value(): string {
|
||||
return this.props.value;
|
||||
}
|
||||
|
||||
equals(other: IValueObject<PageViewIdProps>): boolean {
|
||||
return this.props.value === other.props.value;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user