feat(matching): add auto-matching system for recording partners
Implement algorithm to match dancers with recorders based on: - Heat collision avoidance (division + competitionType + heatNumber) - Buffer time (1 heat after dancing before can record) - Location preference (same city > same country > anyone) - Max 3 recordings per person - Opt-out support (falls to bottom of queue) New API endpoints: - PUT /events/:slug/registration-deadline - PUT /events/:slug/recorder-opt-out - POST /events/:slug/run-matching - GET /events/:slug/match-suggestions - PUT /events/:slug/match-suggestions/:id/status Database changes: - Event: registrationDeadline, matchingRunAt - EventParticipant: recorderOptOut - RecordingSuggestion: new model for match suggestions
This commit is contained in:
@@ -0,0 +1,37 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "event_participants" ADD COLUMN "recorder_opt_out" BOOLEAN NOT NULL DEFAULT false;
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "events" ADD COLUMN "matching_run_at" TIMESTAMP(3),
|
||||
ADD COLUMN "registration_deadline" TIMESTAMP(3);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "recording_suggestions" (
|
||||
"id" SERIAL NOT NULL,
|
||||
"event_id" INTEGER NOT NULL,
|
||||
"heat_id" INTEGER NOT NULL,
|
||||
"recorder_id" INTEGER,
|
||||
"status" VARCHAR(20) NOT NULL DEFAULT 'pending',
|
||||
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updated_at" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "recording_suggestions_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "recording_suggestions_heat_id_key" ON "recording_suggestions"("heat_id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "recording_suggestions_event_id_idx" ON "recording_suggestions"("event_id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "recording_suggestions_recorder_id_idx" ON "recording_suggestions"("recorder_id");
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "recording_suggestions" ADD CONSTRAINT "recording_suggestions_event_id_fkey" FOREIGN KEY ("event_id") REFERENCES "events"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "recording_suggestions" ADD CONSTRAINT "recording_suggestions_heat_id_fkey" FOREIGN KEY ("heat_id") REFERENCES "event_user_heats"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "recording_suggestions" ADD CONSTRAINT "recording_suggestions_recorder_id_fkey" FOREIGN KEY ("recorder_id") REFERENCES "users"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
@@ -59,29 +59,35 @@ model User {
|
||||
ratingsReceived Rating[] @relation("RatedRatings")
|
||||
eventParticipants EventParticipant[]
|
||||
heats EventUserHeat[]
|
||||
recordingAssignments RecordingSuggestion[] @relation("RecorderAssignments")
|
||||
|
||||
@@map("users")
|
||||
}
|
||||
|
||||
// Events table (dance events from worldsdc.com)
|
||||
model Event {
|
||||
id Int @id @default(autoincrement())
|
||||
slug String @unique @default(cuid()) @db.VarChar(50)
|
||||
name String @db.VarChar(255)
|
||||
location String @db.VarChar(255)
|
||||
startDate DateTime @map("start_date") @db.Date
|
||||
endDate DateTime @map("end_date") @db.Date
|
||||
worldsdcId String? @unique @map("worldsdc_id") @db.VarChar(100)
|
||||
participantsCount Int @default(0) @map("participants_count")
|
||||
description String? @db.Text
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
id Int @id @default(autoincrement())
|
||||
slug String @unique @default(cuid()) @db.VarChar(50)
|
||||
name String @db.VarChar(255)
|
||||
location String @db.VarChar(255)
|
||||
startDate DateTime @map("start_date") @db.Date
|
||||
endDate DateTime @map("end_date") @db.Date
|
||||
worldsdcId String? @unique @map("worldsdc_id") @db.VarChar(100)
|
||||
participantsCount Int @default(0) @map("participants_count")
|
||||
description String? @db.Text
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
|
||||
// Auto-matching configuration
|
||||
registrationDeadline DateTime? @map("registration_deadline") // When registration closes
|
||||
matchingRunAt DateTime? @map("matching_run_at") // When auto-matching was last run
|
||||
|
||||
// Relations
|
||||
chatRooms ChatRoom[]
|
||||
matches Match[]
|
||||
participants EventParticipant[]
|
||||
checkinToken EventCheckinToken?
|
||||
userHeats EventUserHeat[]
|
||||
chatRooms ChatRoom[]
|
||||
matches Match[]
|
||||
participants EventParticipant[]
|
||||
checkinToken EventCheckinToken?
|
||||
userHeats EventUserHeat[]
|
||||
recordingSuggestions RecordingSuggestion[]
|
||||
|
||||
@@map("events")
|
||||
}
|
||||
@@ -186,6 +192,7 @@ model EventParticipant {
|
||||
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")
|
||||
|
||||
// Relations
|
||||
@@ -236,10 +243,11 @@ model EventUserHeat {
|
||||
updatedAt DateTime @updatedAt @map("updated_at")
|
||||
|
||||
// Relations
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
event Event @relation(fields: [eventId], references: [id], onDelete: Cascade)
|
||||
division Division @relation(fields: [divisionId], references: [id])
|
||||
competitionType CompetitionType @relation(fields: [competitionTypeId], references: [id])
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
event Event @relation(fields: [eventId], references: [id], onDelete: Cascade)
|
||||
division Division @relation(fields: [divisionId], references: [id])
|
||||
competitionType CompetitionType @relation(fields: [competitionTypeId], references: [id])
|
||||
recordingSuggestion RecordingSuggestion?
|
||||
|
||||
// Constraint: Cannot have same role in same division+competition type
|
||||
@@unique([userId, eventId, divisionId, competitionTypeId, role])
|
||||
@@ -247,3 +255,23 @@ model EventUserHeat {
|
||||
@@index([eventId])
|
||||
@@map("event_user_heats")
|
||||
}
|
||||
|
||||
// Recording suggestions from auto-matching algorithm
|
||||
model RecordingSuggestion {
|
||||
id Int @id @default(autoincrement())
|
||||
eventId Int @map("event_id")
|
||||
heatId Int @unique @map("heat_id") // One suggestion per heat
|
||||
recorderId Int? @map("recorder_id") // NULL if no match found
|
||||
status String @default("pending") @db.VarChar(20) // 'pending', 'accepted', 'rejected', 'not_found'
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
updatedAt DateTime @updatedAt @map("updated_at")
|
||||
|
||||
// Relations
|
||||
event Event @relation(fields: [eventId], references: [id], onDelete: Cascade)
|
||||
heat EventUserHeat @relation(fields: [heatId], references: [id], onDelete: Cascade)
|
||||
recorder User? @relation("RecorderAssignments", fields: [recorderId], references: [id])
|
||||
|
||||
@@index([eventId])
|
||||
@@index([recorderId])
|
||||
@@map("recording_suggestions")
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user