/** * 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 '@core/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 { 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 & { 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): 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)}`; } }