team rating
This commit is contained in:
162
core/racing/domain/services/TeamRatingSnapshotCalculator.ts
Normal file
162
core/racing/domain/services/TeamRatingSnapshotCalculator.ts
Normal file
@@ -0,0 +1,162 @@
|
||||
import { TeamRatingEvent } from '../entities/TeamRatingEvent';
|
||||
import { TeamRatingValue } from '../value-objects/TeamRatingValue';
|
||||
import { TeamRatingDimensionKey } from '../value-objects/TeamRatingDimensionKey';
|
||||
|
||||
export interface TeamRatingSnapshot {
|
||||
teamId: string;
|
||||
driving: TeamRatingValue;
|
||||
adminTrust: TeamRatingValue;
|
||||
overall: number; // Calculated overall rating
|
||||
lastUpdated: Date;
|
||||
eventCount: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Domain Service: TeamRatingSnapshotCalculator
|
||||
*
|
||||
* Calculates team rating snapshots from event ledgers.
|
||||
* Mirrors the user RatingSnapshotCalculator pattern.
|
||||
*
|
||||
* Pure domain logic - no persistence concerns.
|
||||
*/
|
||||
export class TeamRatingSnapshotCalculator {
|
||||
/**
|
||||
* Calculate current team rating snapshot from all events.
|
||||
*
|
||||
* @param teamId - The team ID to calculate for
|
||||
* @param events - All rating events for the team
|
||||
* @returns TeamRatingSnapshot with current ratings
|
||||
*/
|
||||
static calculate(teamId: string, events: TeamRatingEvent[]): TeamRatingSnapshot {
|
||||
// Start with default ratings (50 for each dimension)
|
||||
const defaultRating = 50;
|
||||
|
||||
if (events.length === 0) {
|
||||
return {
|
||||
teamId,
|
||||
driving: TeamRatingValue.create(defaultRating),
|
||||
adminTrust: TeamRatingValue.create(defaultRating),
|
||||
overall: defaultRating,
|
||||
lastUpdated: new Date(),
|
||||
eventCount: 0,
|
||||
};
|
||||
}
|
||||
|
||||
// Group events by dimension
|
||||
const eventsByDimension = events.reduce((acc, event) => {
|
||||
const key = event.dimension.value;
|
||||
if (!acc[key]) {
|
||||
acc[key] = [];
|
||||
}
|
||||
acc[key].push(event);
|
||||
return acc;
|
||||
}, {} as Record<string, TeamRatingEvent[]>);
|
||||
|
||||
// Calculate each dimension
|
||||
const dimensionRatings: Record<string, number> = {};
|
||||
|
||||
for (const [dimensionKey, dimensionEvents] of Object.entries(eventsByDimension)) {
|
||||
const totalWeight = dimensionEvents.reduce((sum, event) => {
|
||||
return sum + (event.weight || 1);
|
||||
}, 0);
|
||||
|
||||
const weightedSum = dimensionEvents.reduce((sum, event) => {
|
||||
return sum + (event.delta.value * (event.weight || 1));
|
||||
}, 0);
|
||||
|
||||
// Normalize and add to base rating
|
||||
const normalizedDelta = weightedSum / totalWeight;
|
||||
dimensionRatings[dimensionKey] = Math.max(0, Math.min(100, defaultRating + normalizedDelta));
|
||||
}
|
||||
|
||||
const drivingRating = dimensionRatings['driving'] ?? defaultRating;
|
||||
const adminTrustRating = dimensionRatings['adminTrust'] ?? defaultRating;
|
||||
|
||||
// Calculate overall as weighted average
|
||||
const overall = (drivingRating * 0.7 + adminTrustRating * 0.3);
|
||||
|
||||
// Find latest event date
|
||||
const lastUpdated = events.reduce((latest, event) => {
|
||||
return event.occurredAt > latest ? event.occurredAt : latest;
|
||||
}, new Date(0));
|
||||
|
||||
return {
|
||||
teamId,
|
||||
driving: TeamRatingValue.create(drivingRating),
|
||||
adminTrust: TeamRatingValue.create(adminTrustRating),
|
||||
overall: Math.round(overall * 10) / 10, // Round to 1 decimal
|
||||
lastUpdated,
|
||||
eventCount: events.length,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate rating change for a specific dimension from events.
|
||||
*
|
||||
* @param dimension - The dimension to calculate for
|
||||
* @param events - Events to calculate from
|
||||
* @returns Net change value
|
||||
*/
|
||||
static calculateDimensionChange(
|
||||
dimension: TeamRatingDimensionKey,
|
||||
events: TeamRatingEvent[]
|
||||
): number {
|
||||
const filtered = events.filter(e => e.dimension.equals(dimension));
|
||||
|
||||
if (filtered.length === 0) return 0;
|
||||
|
||||
const totalWeight = filtered.reduce((sum, event) => {
|
||||
return sum + (event.weight || 1);
|
||||
}, 0);
|
||||
|
||||
const weightedSum = filtered.reduce((sum, event) => {
|
||||
return sum + (event.delta.value * (event.weight || 1));
|
||||
}, 0);
|
||||
|
||||
return weightedSum / totalWeight;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate rating change over a time window.
|
||||
*
|
||||
* @param teamId - The team ID
|
||||
* @param events - All events
|
||||
* @param from - Start date
|
||||
* @param to - End date
|
||||
* @returns Snapshot of ratings at the end of the window
|
||||
*/
|
||||
static calculateOverWindow(
|
||||
teamId: string,
|
||||
events: TeamRatingEvent[],
|
||||
from: Date,
|
||||
to: Date
|
||||
): TeamRatingSnapshot {
|
||||
const windowEvents = events.filter(e =>
|
||||
e.occurredAt >= from && e.occurredAt <= to
|
||||
);
|
||||
|
||||
return this.calculate(teamId, windowEvents);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate rating change between two snapshots.
|
||||
*
|
||||
* @param before - Snapshot before changes
|
||||
* @param after - Snapshot after changes
|
||||
* @returns Object with change values
|
||||
*/
|
||||
static calculateDelta(
|
||||
before: TeamRatingSnapshot,
|
||||
after: TeamRatingSnapshot
|
||||
): {
|
||||
driving: number;
|
||||
adminTrust: number;
|
||||
overall: number;
|
||||
} {
|
||||
return {
|
||||
driving: after.driving.value - before.driving.value,
|
||||
adminTrust: after.adminTrust.value - before.adminTrust.value,
|
||||
overall: after.overall - before.overall,
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user