wip
This commit is contained in:
255
packages/identity/domain/value-objects/UserRating.ts
Normal file
255
packages/identity/domain/value-objects/UserRating.ts
Normal file
@@ -0,0 +1,255 @@
|
||||
/**
|
||||
* Value Object: UserRating
|
||||
*
|
||||
* Multi-dimensional rating system for users covering:
|
||||
* - Driver skill: racing ability, lap times, consistency
|
||||
* - Admin competence: league management, event organization
|
||||
* - Steward fairness: protest handling, penalty consistency
|
||||
* - Trust score: reliability, sportsmanship, rule compliance
|
||||
* - Fairness score: clean racing, incident involvement
|
||||
*/
|
||||
|
||||
export interface RatingDimension {
|
||||
value: number; // Current rating value (0-100 scale)
|
||||
confidence: number; // Confidence level based on sample size (0-1)
|
||||
sampleSize: number; // Number of events contributing to this rating
|
||||
trend: 'rising' | 'stable' | 'falling';
|
||||
lastUpdated: Date;
|
||||
}
|
||||
|
||||
export interface UserRatingProps {
|
||||
userId: string;
|
||||
driver: RatingDimension;
|
||||
admin: RatingDimension;
|
||||
steward: RatingDimension;
|
||||
trust: RatingDimension;
|
||||
fairness: RatingDimension;
|
||||
overallReputation: number;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
}
|
||||
|
||||
const DEFAULT_DIMENSION: RatingDimension = {
|
||||
value: 50,
|
||||
confidence: 0,
|
||||
sampleSize: 0,
|
||||
trend: 'stable',
|
||||
lastUpdated: new Date(),
|
||||
};
|
||||
|
||||
export class UserRating {
|
||||
readonly userId: string;
|
||||
readonly driver: RatingDimension;
|
||||
readonly admin: RatingDimension;
|
||||
readonly steward: RatingDimension;
|
||||
readonly trust: RatingDimension;
|
||||
readonly fairness: RatingDimension;
|
||||
readonly overallReputation: number;
|
||||
readonly createdAt: Date;
|
||||
readonly updatedAt: Date;
|
||||
|
||||
private constructor(props: UserRatingProps) {
|
||||
this.userId = props.userId;
|
||||
this.driver = props.driver;
|
||||
this.admin = props.admin;
|
||||
this.steward = props.steward;
|
||||
this.trust = props.trust;
|
||||
this.fairness = props.fairness;
|
||||
this.overallReputation = props.overallReputation;
|
||||
this.createdAt = props.createdAt;
|
||||
this.updatedAt = props.updatedAt;
|
||||
}
|
||||
|
||||
static create(userId: string): UserRating {
|
||||
if (!userId || userId.trim().length === 0) {
|
||||
throw new Error('UserRating userId is required');
|
||||
}
|
||||
|
||||
const now = new Date();
|
||||
return new UserRating({
|
||||
userId,
|
||||
driver: { ...DEFAULT_DIMENSION, lastUpdated: now },
|
||||
admin: { ...DEFAULT_DIMENSION, lastUpdated: now },
|
||||
steward: { ...DEFAULT_DIMENSION, lastUpdated: now },
|
||||
trust: { ...DEFAULT_DIMENSION, lastUpdated: now },
|
||||
fairness: { ...DEFAULT_DIMENSION, lastUpdated: now },
|
||||
overallReputation: 50,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
});
|
||||
}
|
||||
|
||||
static restore(props: UserRatingProps): UserRating {
|
||||
return new UserRating(props);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update driver rating based on race performance
|
||||
*/
|
||||
updateDriverRating(
|
||||
newValue: number,
|
||||
weight: number = 1
|
||||
): UserRating {
|
||||
const updated = this.updateDimension(this.driver, newValue, weight);
|
||||
return this.withUpdates({ driver: updated });
|
||||
}
|
||||
|
||||
/**
|
||||
* Update admin rating based on league management feedback
|
||||
*/
|
||||
updateAdminRating(
|
||||
newValue: number,
|
||||
weight: number = 1
|
||||
): UserRating {
|
||||
const updated = this.updateDimension(this.admin, newValue, weight);
|
||||
return this.withUpdates({ admin: updated });
|
||||
}
|
||||
|
||||
/**
|
||||
* Update steward rating based on protest handling feedback
|
||||
*/
|
||||
updateStewardRating(
|
||||
newValue: number,
|
||||
weight: number = 1
|
||||
): UserRating {
|
||||
const updated = this.updateDimension(this.steward, newValue, weight);
|
||||
return this.withUpdates({ steward: updated });
|
||||
}
|
||||
|
||||
/**
|
||||
* Update trust score based on reliability and sportsmanship
|
||||
*/
|
||||
updateTrustScore(
|
||||
newValue: number,
|
||||
weight: number = 1
|
||||
): UserRating {
|
||||
const updated = this.updateDimension(this.trust, newValue, weight);
|
||||
return this.withUpdates({ trust: updated });
|
||||
}
|
||||
|
||||
/**
|
||||
* Update fairness score based on clean racing incidents
|
||||
*/
|
||||
updateFairnessScore(
|
||||
newValue: number,
|
||||
weight: number = 1
|
||||
): UserRating {
|
||||
const updated = this.updateDimension(this.fairness, newValue, weight);
|
||||
return this.withUpdates({ fairness: updated });
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate weighted overall reputation
|
||||
*/
|
||||
calculateOverallReputation(): number {
|
||||
// Weight dimensions by confidence and importance
|
||||
const weights = {
|
||||
driver: 0.25 * this.driver.confidence,
|
||||
admin: 0.15 * this.admin.confidence,
|
||||
steward: 0.15 * this.steward.confidence,
|
||||
trust: 0.25 * this.trust.confidence,
|
||||
fairness: 0.20 * this.fairness.confidence,
|
||||
};
|
||||
|
||||
const totalWeight = Object.values(weights).reduce((sum, w) => sum + w, 0);
|
||||
|
||||
if (totalWeight === 0) {
|
||||
return 50; // Default when no ratings yet
|
||||
}
|
||||
|
||||
const weightedSum =
|
||||
this.driver.value * weights.driver +
|
||||
this.admin.value * weights.admin +
|
||||
this.steward.value * weights.steward +
|
||||
this.trust.value * weights.trust +
|
||||
this.fairness.value * weights.fairness;
|
||||
|
||||
return Math.round(weightedSum / totalWeight);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get rating tier for display
|
||||
*/
|
||||
getDriverTier(): 'rookie' | 'amateur' | 'semi-pro' | 'pro' | 'elite' {
|
||||
if (this.driver.value >= 90) return 'elite';
|
||||
if (this.driver.value >= 75) return 'pro';
|
||||
if (this.driver.value >= 60) return 'semi-pro';
|
||||
if (this.driver.value >= 40) return 'amateur';
|
||||
return 'rookie';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get trust level for matchmaking
|
||||
*/
|
||||
getTrustLevel(): 'unverified' | 'trusted' | 'highly-trusted' | 'community-leader' {
|
||||
if (this.trust.value >= 90 && this.trust.sampleSize >= 50) return 'community-leader';
|
||||
if (this.trust.value >= 75 && this.trust.sampleSize >= 20) return 'highly-trusted';
|
||||
if (this.trust.value >= 60 && this.trust.sampleSize >= 5) return 'trusted';
|
||||
return 'unverified';
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if user is eligible to be a steward
|
||||
*/
|
||||
canBeSteward(): boolean {
|
||||
return (
|
||||
this.trust.value >= 70 &&
|
||||
this.fairness.value >= 70 &&
|
||||
this.trust.sampleSize >= 10
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if user is eligible to be an admin
|
||||
*/
|
||||
canBeAdmin(): boolean {
|
||||
return (
|
||||
this.trust.value >= 60 &&
|
||||
this.trust.sampleSize >= 5
|
||||
);
|
||||
}
|
||||
|
||||
private updateDimension(
|
||||
dimension: RatingDimension,
|
||||
newValue: number,
|
||||
weight: number
|
||||
): RatingDimension {
|
||||
const clampedValue = Math.max(0, Math.min(100, newValue));
|
||||
const newSampleSize = dimension.sampleSize + weight;
|
||||
|
||||
// Exponential moving average with decay based on sample size
|
||||
const alpha = Math.min(0.3, 1 / (dimension.sampleSize + 1));
|
||||
const updatedValue = dimension.value * (1 - alpha) + clampedValue * alpha;
|
||||
|
||||
// Calculate confidence (asymptotic to 1)
|
||||
const confidence = 1 - Math.exp(-newSampleSize / 20);
|
||||
|
||||
// Determine trend
|
||||
const valueDiff = updatedValue - dimension.value;
|
||||
let trend: 'rising' | 'stable' | 'falling' = 'stable';
|
||||
if (valueDiff > 2) trend = 'rising';
|
||||
if (valueDiff < -2) trend = 'falling';
|
||||
|
||||
return {
|
||||
value: Math.round(updatedValue * 10) / 10,
|
||||
confidence: Math.round(confidence * 100) / 100,
|
||||
sampleSize: newSampleSize,
|
||||
trend,
|
||||
lastUpdated: new Date(),
|
||||
};
|
||||
}
|
||||
|
||||
private withUpdates(updates: Partial<UserRatingProps>): UserRating {
|
||||
const newRating = new UserRating({
|
||||
...this,
|
||||
...updates,
|
||||
updatedAt: new Date(),
|
||||
});
|
||||
|
||||
// Recalculate overall reputation
|
||||
return new UserRating({
|
||||
...newRating,
|
||||
overallReputation: newRating.calculateOverallReputation(),
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user