148 lines
4.0 KiB
TypeScript
148 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 { Entity } from '@core/shared/domain/Entity';
|
|
import type {
|
|
AnalyticsMetrics,
|
|
AnalyticsSnapshotProps,
|
|
SnapshotEntityType,
|
|
SnapshotPeriod,
|
|
} from '../types/AnalyticsSnapshot';
|
|
import { AnalyticsEntityId } from '../value-objects/AnalyticsEntityId';
|
|
export type { SnapshotEntityType, SnapshotPeriod } from '../types/AnalyticsSnapshot';
|
|
|
|
export class AnalyticsSnapshot extends Entity<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) {
|
|
super(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)}`;
|
|
}
|
|
} |