feat(matching): implement 3-tier account system with fairness-based recording assignment

Add account tier system (BASIC/SUPPORTER/COMFORT) to reduce recording burden
for premium users while maintaining fairness through karma-based assignment.

Database Changes:
- Add AccountTier enum (BASIC, SUPPORTER, COMFORT)
- Add User.accountTier with BASIC default
- Add User.recordingsDone and User.recordingsReceived for karma tracking
- Add EventParticipant.accountTierOverride for event-specific tier upgrades
- Migration: 20251129220604_add_account_tiers_and_recording_stats

Matching Algorithm Updates:
- Implement fairness debt calculation: receivedCount - doneCount
- Apply tier penalties: SUPPORTER (-10), COMFORT (-50)
- New sorting priority: Location > Fairness > Load balancing
- Add getEffectiveTier() helper for tier resolution with override support
- Add getRecordingStatsForUsers() for fetching karma statistics

Tier Behavior:
- BASIC: Normal recording frequency (baseline, no penalty)
- SUPPORTER: Moderately reduced frequency (fairness penalty -10)
- COMFORT: Significantly reduced frequency (fairness penalty -50)
- All tiers can still be assigned when no better candidates available

Constants:
- ACCOUNT_TIER enum in src/constants/tiers.js
- FAIRNESS_SUPPORTER_PENALTY = 10
- FAIRNESS_COMFORT_PENALTY = 50

Tests:
- Update tests for dual buffer system semantics
- All 30 tests passing
- Fix imports: HEAT_BUFFER → HEAT_BUFFER_BEFORE
This commit is contained in:
Radosław Gierwiało
2025-11-29 23:19:41 +01:00
parent 029b25c9b2
commit aef1a35ee2
6 changed files with 158 additions and 28 deletions

View File

@@ -11,6 +11,13 @@ datasource db {
url = env("DATABASE_URL")
}
// Account tier enum
enum AccountTier {
BASIC
SUPPORTER
COMFORT
}
// Users table
model User {
id Int @id @default(autoincrement())
@@ -47,9 +54,15 @@ model User {
failedLoginAttempts Int @default(0) @map("failed_login_attempts")
lockedUntil DateTime? @map("locked_until")
avatar String? @db.VarChar(255)
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
avatar String? @db.VarChar(255)
// Account Tier & Recording Stats
accountTier AccountTier @default(BASIC) @map("account_tier")
recordingsDone Int @default(0) @map("recordings_done") // How many times this user recorded others
recordingsReceived Int @default(0) @map("recordings_received") // How many times others recorded this user
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
// Relations
messages Message[]
@@ -189,12 +202,13 @@ model Rating {
// Event participants (tracks which users joined which events)
model EventParticipant {
id Int @id @default(autoincrement())
userId Int @map("user_id")
eventId Int @map("event_id")
competitorNumber Int? @map("competitor_number") // Bib number - one per user per event
recorderOptOut Boolean @default(false) @map("recorder_opt_out") // Opt-out from being a recorder
joinedAt DateTime @default(now()) @map("joined_at")
id Int @id @default(autoincrement())
userId Int @map("user_id")
eventId Int @map("event_id")
competitorNumber Int? @map("competitor_number") // Bib number - one per user per event
recorderOptOut Boolean @default(false) @map("recorder_opt_out") // Opt-out from being a recorder
accountTierOverride AccountTier? @map("account_tier_override") // Override user's global tier for this event (e.g., Comfort Pass)
joinedAt DateTime @default(now()) @map("joined_at")
// Relations
user User @relation(fields: [userId], references: [id], onDelete: Cascade)