rename to core

This commit is contained in:
2025-12-15 13:46:07 +01:00
parent aedf58643d
commit 5c22f8820c
559 changed files with 415 additions and 767 deletions

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

View 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;
}
}

View 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');
}
}