Files
gridpilot.gg/core/analytics/domain/entities/AnalyticsSnapshot.ts
2025-12-15 13:46:07 +01:00

149 lines
4.0 KiB
TypeScript

/**
* 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)}`;
}
}