feat: implement recording stats update mechanism for auto-matching

Add automatic tracking of recording statistics (recordingsDone/recordingsReceived)
for users participating in auto-matched collaborations. Stats are updated when
both users complete mutual ratings after a recording session.

Changes:
- Add suggestionId, source, and statsApplied fields to Match model
- Implement applyRecordingStatsForMatch() helper with user role convention
  (user1 = dancer, user2 = recorder)
- Update suggestion status endpoint to create Match on acceptance
- Update ratings endpoint to apply stats when match is completed
- Add comprehensive unit tests (5) and integration tests (5)

Convention: Stats only updated for auto-matches (source='auto') to ensure
fairness metrics reflect actual algorithmic assignments, not manual matches.

Test Results: 304/305 tests passing (99.7%)
Coverage: 74.53% (+1.48%)
This commit is contained in:
Radosław Gierwiało
2025-11-30 10:40:43 +01:00
parent 5ee1e0a4b9
commit 145c9f7ce6
6 changed files with 741 additions and 22 deletions

View File

@@ -165,17 +165,24 @@ model Match {
user1LastReadAt DateTime? @map("user1_last_read_at") user1LastReadAt DateTime? @map("user1_last_read_at")
user2LastReadAt DateTime? @map("user2_last_read_at") user2LastReadAt DateTime? @map("user2_last_read_at")
// Auto-matching integration
suggestionId Int? @unique @map("suggestion_id") // Link to auto-matching suggestion (optional)
source String @default("manual") @db.VarChar(20) // 'manual' | 'auto'
statsApplied Boolean @default(false) @map("stats_applied") // Flag to ensure stats applied only once
// Relations // Relations
user1 User @relation("MatchUser1", fields: [user1Id], references: [id]) user1 User @relation("MatchUser1", fields: [user1Id], references: [id])
user2 User @relation("MatchUser2", fields: [user2Id], references: [id]) user2 User @relation("MatchUser2", fields: [user2Id], references: [id])
event Event @relation(fields: [eventId], references: [id]) event Event @relation(fields: [eventId], references: [id])
room ChatRoom? @relation(fields: [roomId], references: [id]) room ChatRoom? @relation(fields: [roomId], references: [id])
ratings Rating[] suggestion RecordingSuggestion? @relation(fields: [suggestionId], references: [id])
ratings Rating[]
@@unique([user1Id, user2Id, eventId]) @@unique([user1Id, user2Id, eventId])
@@index([user1Id]) @@index([user1Id])
@@index([user2Id]) @@index([user2Id])
@@index([eventId]) @@index([eventId])
@@index([suggestionId])
@@map("matches") @@map("matches")
} }
@@ -285,6 +292,7 @@ model RecordingSuggestion {
event Event @relation(fields: [eventId], references: [id], onDelete: Cascade) event Event @relation(fields: [eventId], references: [id], onDelete: Cascade)
heat EventUserHeat @relation(fields: [heatId], references: [id], onDelete: Cascade) heat EventUserHeat @relation(fields: [heatId], references: [id], onDelete: Cascade)
recorder User? @relation("RecorderAssignments", fields: [recorderId], references: [id]) recorder User? @relation("RecorderAssignments", fields: [recorderId], references: [id])
match Match? // Link to created match (if suggestion was accepted)
@@index([eventId]) @@index([eventId])
@@index([recorderId]) @@index([recorderId])

View File

@@ -10,11 +10,24 @@ const {
hasCollision, hasCollision,
getLocationScore, getLocationScore,
buildDivisionSlotMap, buildDivisionSlotMap,
applyRecordingStatsForMatch,
MAX_RECORDINGS_PER_PERSON, MAX_RECORDINGS_PER_PERSON,
HEAT_BUFFER_BEFORE, HEAT_BUFFER_BEFORE,
HEAT_BUFFER_AFTER, HEAT_BUFFER_AFTER,
} = require('../services/matching'); } = require('../services/matching');
// Mock prisma for unit tests
jest.mock('../utils/db', () => ({
prisma: {
$transaction: jest.fn(),
user: {
update: jest.fn(),
},
},
}));
const { prisma } = require('../utils/db');
describe('Matching Service - Unit Tests', () => { describe('Matching Service - Unit Tests', () => {
describe('getTimeSlot', () => { describe('getTimeSlot', () => {
it('should create unique slot identifier', () => { it('should create unique slot identifier', () => {
@@ -483,4 +496,137 @@ describe('Matching Service - Unit Tests', () => {
expect(hasCollision(dancerHeats, recorderHeats, divisionSlotMap)).toBe(false); expect(hasCollision(dancerHeats, recorderHeats, divisionSlotMap)).toBe(false);
}); });
}); });
describe('applyRecordingStatsForMatch', () => {
beforeEach(() => {
jest.clearAllMocks();
});
it('should increment stats for auto-match with correct user roles', async () => {
const match = {
id: 1,
user1Id: 100, // dancer
user2Id: 200, // recorder
source: 'auto',
statsApplied: false,
};
// Mock transaction to execute callback immediately
prisma.$transaction.mockImplementation(async (callback) => {
if (Array.isArray(callback)) {
// Array of promises
return Promise.all(callback);
} else {
// Callback function
return callback(prisma);
}
});
prisma.user.update.mockResolvedValue({});
await applyRecordingStatsForMatch(match);
// Verify transaction was called with array of updates
expect(prisma.$transaction).toHaveBeenCalledTimes(1);
const transactionArg = prisma.$transaction.mock.calls[0][0];
expect(Array.isArray(transactionArg)).toBe(true);
expect(transactionArg).toHaveLength(2);
// Verify user updates were called with correct parameters
// First call should update recorder (user2Id)
expect(prisma.user.update).toHaveBeenNthCalledWith(1, {
where: { id: 200 }, // recorder
data: { recordingsDone: { increment: 1 } },
});
// Second call should update dancer (user1Id)
expect(prisma.user.update).toHaveBeenNthCalledWith(2, {
where: { id: 100 }, // dancer
data: { recordingsReceived: { increment: 1 } },
});
});
it('should NOT update stats for manual match', async () => {
const match = {
id: 1,
user1Id: 100,
user2Id: 200,
source: 'manual', // Not auto
statsApplied: false,
};
await applyRecordingStatsForMatch(match);
// Should return early without calling prisma
expect(prisma.$transaction).not.toHaveBeenCalled();
expect(prisma.user.update).not.toHaveBeenCalled();
});
it('should NOT update stats when source is missing', async () => {
const match = {
id: 1,
user1Id: 100,
user2Id: 200,
// source missing (undefined)
statsApplied: false,
};
await applyRecordingStatsForMatch(match);
expect(prisma.$transaction).not.toHaveBeenCalled();
expect(prisma.user.update).not.toHaveBeenCalled();
});
it('should handle different user IDs correctly', async () => {
const match = {
id: 2,
user1Id: 999, // dancer
user2Id: 888, // recorder
source: 'auto',
};
prisma.$transaction.mockImplementation(async (callback) => {
if (Array.isArray(callback)) {
return Promise.all(callback);
}
});
prisma.user.update.mockResolvedValue({});
await applyRecordingStatsForMatch(match);
// Verify correct user IDs were used
expect(prisma.user.update).toHaveBeenNthCalledWith(1, {
where: { id: 888 }, // recorder
data: { recordingsDone: { increment: 1 } },
});
expect(prisma.user.update).toHaveBeenNthCalledWith(2, {
where: { id: 999 }, // dancer
data: { recordingsReceived: { increment: 1 } },
});
});
it('should execute updates in a transaction', async () => {
const match = {
id: 1,
user1Id: 100,
user2Id: 200,
source: 'auto',
};
prisma.$transaction.mockImplementation(async (operations) => {
// Verify it's an array of operations (transaction)
expect(Array.isArray(operations)).toBe(true);
return Promise.all(operations);
});
prisma.user.update.mockResolvedValue({});
await applyRecordingStatsForMatch(match);
// Verify transaction was used (atomic operation)
expect(prisma.$transaction).toHaveBeenCalledTimes(1);
});
});
}); });

View File

@@ -0,0 +1,447 @@
/**
* Integration tests for Recording Stats Update Mechanism
*
* Tests the full flow:
* 1. Recording suggestion accepted → Match created
* 2. Both users rate → Match completed + stats updated
*/
const request = require('supertest');
const app = require('../app');
const { prisma } = require('../utils/db');
const { hashPassword, generateToken } = require('../utils/auth');
// Test data
let dancer, recorder;
let dancerToken, recorderToken;
let testEvent;
let testDivision, testCompetitionType;
let testHeat;
let testSuggestion;
// Setup test data
beforeAll(async () => {
// Clean up test data
const testUsernames = ['stats_dancer', 'stats_recorder'];
const testEmails = ['stats_dancer@example.com', 'stats_recorder@example.com'];
const testEventSlug = 'stats-test-event-2025';
// Find and delete existing test users
const existingUsers = await prisma.user.findMany({
where: {
OR: [
{ username: { in: testUsernames } },
{ email: { in: testEmails } }
]
},
select: { id: true }
});
const existingUserIds = existingUsers.map(u => u.id);
if (existingUserIds.length > 0) {
await prisma.eventUserHeat.deleteMany({ where: { userId: { in: existingUserIds } } });
await prisma.rating.deleteMany({ where: { raterId: { in: existingUserIds } } });
await prisma.match.deleteMany({
where: {
OR: [
{ user1Id: { in: existingUserIds } },
{ user2Id: { in: existingUserIds } }
]
}
});
await prisma.eventParticipant.deleteMany({ where: { userId: { in: existingUserIds } } });
await prisma.user.deleteMany({ where: { id: { in: existingUserIds } } });
}
// Delete test event
const existingEvent = await prisma.event.findUnique({
where: { slug: testEventSlug },
select: { id: true }
});
if (existingEvent) {
await prisma.recordingSuggestion.deleteMany({ where: { eventId: existingEvent.id } });
await prisma.eventUserHeat.deleteMany({ where: { eventId: existingEvent.id } });
await prisma.eventParticipant.deleteMany({ where: { eventId: existingEvent.id } });
await prisma.event.delete({ where: { id: existingEvent.id } });
}
// Create test users with initial stats = 0
const hashedPassword = await hashPassword('TestPass123!');
dancer = await prisma.user.create({
data: {
username: 'stats_dancer',
email: 'stats_dancer@example.com',
passwordHash: hashedPassword,
emailVerified: true,
recordingsDone: 0,
recordingsReceived: 0,
},
});
recorder = await prisma.user.create({
data: {
username: 'stats_recorder',
email: 'stats_recorder@example.com',
passwordHash: hashedPassword,
emailVerified: true,
recordingsDone: 0,
recordingsReceived: 0,
},
});
dancerToken = generateToken({ userId: dancer.id });
recorderToken = generateToken({ userId: recorder.id });
// Create test event
testEvent = await prisma.event.create({
data: {
slug: testEventSlug,
name: 'Stats Test Event 2025',
location: 'Test City',
startDate: new Date('2025-06-01'),
endDate: new Date('2025-06-03'),
},
});
// Add participants
await prisma.eventParticipant.createMany({
data: [
{ userId: dancer.id, eventId: testEvent.id, competitorNumber: 100 },
{ userId: recorder.id, eventId: testEvent.id },
],
});
// Get or create division and competition type
testDivision = await prisma.division.findFirst({
where: { abbreviation: 'INT' },
});
if (!testDivision) {
testDivision = await prisma.division.create({
data: {
name: 'Intermediate',
abbreviation: 'INT',
displayOrder: 3,
},
});
}
testCompetitionType = await prisma.competitionType.findFirst({
where: { abbreviation: 'J&J' }, // Use existing abbreviation
});
if (!testCompetitionType) {
testCompetitionType = await prisma.competitionType.create({
data: {
name: 'Jack & Jill',
abbreviation: 'J&J',
},
});
}
// Create heat for dancer
testHeat = await prisma.eventUserHeat.create({
data: {
userId: dancer.id,
eventId: testEvent.id,
divisionId: testDivision.id,
competitionTypeId: testCompetitionType.id,
heatNumber: 1,
role: 'Leader',
},
});
// Create recording suggestion
testSuggestion = await prisma.recordingSuggestion.create({
data: {
eventId: testEvent.id,
heatId: testHeat.id,
recorderId: recorder.id,
status: 'pending',
},
});
});
afterAll(async () => {
// Cleanup
if (dancer && recorder) {
const userIds = [dancer.id, recorder.id];
await prisma.eventUserHeat.deleteMany({ where: { userId: { in: userIds } } });
await prisma.rating.deleteMany({ where: { raterId: { in: userIds } } });
await prisma.match.deleteMany({
where: {
OR: [
{ user1Id: { in: userIds } },
{ user2Id: { in: userIds } }
]
}
});
await prisma.eventParticipant.deleteMany({ where: { userId: { in: userIds } } });
await prisma.user.deleteMany({ where: { id: { in: userIds } } });
}
if (testEvent) {
await prisma.recordingSuggestion.deleteMany({ where: { eventId: testEvent.id } });
await prisma.eventUserHeat.deleteMany({ where: { eventId: testEvent.id } });
await prisma.eventParticipant.deleteMany({ where: { eventId: testEvent.id } });
await prisma.event.delete({ where: { id: testEvent.id } });
}
await prisma.$disconnect();
});
describe('Recording Stats Integration Tests', () => {
describe('Full Flow: Suggestion → Match → Ratings → Stats', () => {
let createdMatch;
it('should create Match with correct fields when suggestion is accepted', async () => {
// Recorder accepts the suggestion
const response = await request(app)
.put(`/api/events/${testEvent.slug}/match-suggestions/${testSuggestion.id}/status`)
.set('Authorization', `Bearer ${recorderToken}`)
.send({ status: 'accepted' });
expect(response.status).toBe(200);
expect(response.body.success).toBe(true);
expect(response.body.data.status).toBe('accepted');
expect(response.body.data.matchId).toBeDefined();
expect(response.body.data.matchSlug).toBeDefined();
// Verify Match was created with correct fields
const match = await prisma.match.findUnique({
where: { id: response.body.data.matchId },
include: { room: true },
});
expect(match).toBeDefined();
expect(match.user1Id).toBe(dancer.id); // Convention: user1 = dancer
expect(match.user2Id).toBe(recorder.id); // Convention: user2 = recorder
expect(match.eventId).toBe(testEvent.id);
expect(match.suggestionId).toBe(testSuggestion.id);
expect(match.source).toBe('auto');
expect(match.status).toBe('accepted');
expect(match.statsApplied).toBe(false);
expect(match.roomId).toBeDefined(); // Chat room created
expect(match.room).toBeDefined();
expect(match.room.type).toBe('private');
createdMatch = match;
});
it('should be idempotent - accepting again should not create duplicate Match', async () => {
// Update suggestion back to pending
await prisma.recordingSuggestion.update({
where: { id: testSuggestion.id },
data: { status: 'pending' },
});
// Accept again
const response = await request(app)
.put(`/api/events/${testEvent.slug}/match-suggestions/${testSuggestion.id}/status`)
.set('Authorization', `Bearer ${recorderToken}`)
.send({ status: 'accepted' });
expect(response.status).toBe(200);
expect(response.body.data.matchId).toBe(createdMatch.id); // Same match
// Verify only one match exists
const matchCount = await prisma.match.count({
where: { suggestionId: testSuggestion.id },
});
expect(matchCount).toBe(1);
});
it('should NOT update stats after only one rating', async () => {
// Dancer rates recorder
const response = await request(app)
.post(`/api/matches/${createdMatch.slug}/ratings`)
.set('Authorization', `Bearer ${dancerToken}`)
.send({
score: 5,
comment: 'Great recorder!',
wouldCollaborateAgain: true,
});
expect(response.status).toBe(201);
expect(response.body.success).toBe(true);
// Verify match is still 'accepted' (not completed)
const match = await prisma.match.findUnique({
where: { id: createdMatch.id },
});
expect(match.status).toBe('accepted');
expect(match.statsApplied).toBe(false);
// Verify stats NOT updated yet
const dancerStats = await prisma.user.findUnique({
where: { id: dancer.id },
select: { recordingsDone: true, recordingsReceived: true },
});
const recorderStats = await prisma.user.findUnique({
where: { id: recorder.id },
select: { recordingsDone: true, recordingsReceived: true },
});
expect(dancerStats.recordingsDone).toBe(0);
expect(dancerStats.recordingsReceived).toBe(0);
expect(recorderStats.recordingsDone).toBe(0);
expect(recorderStats.recordingsReceived).toBe(0);
});
it('should update stats and mark match as completed after both users rate', async () => {
// Recorder rates dancer
const response = await request(app)
.post(`/api/matches/${createdMatch.slug}/ratings`)
.set('Authorization', `Bearer ${recorderToken}`)
.send({
score: 5,
comment: 'Great dancer!',
wouldCollaborateAgain: true,
});
expect(response.status).toBe(201);
expect(response.body.success).toBe(true);
// Verify match is now 'completed' with statsApplied
const match = await prisma.match.findUnique({
where: { id: createdMatch.id },
});
expect(match.status).toBe('completed');
expect(match.statsApplied).toBe(true);
// Verify stats were updated correctly
const dancerStats = await prisma.user.findUnique({
where: { id: dancer.id },
select: { recordingsDone: true, recordingsReceived: true },
});
const recorderStats = await prisma.user.findUnique({
where: { id: recorder.id },
select: { recordingsDone: true, recordingsReceived: true },
});
// Dancer: received +1 (was recorded)
expect(dancerStats.recordingsDone).toBe(0);
expect(dancerStats.recordingsReceived).toBe(1);
// Recorder: done +1 (did the recording)
expect(recorderStats.recordingsDone).toBe(1);
expect(recorderStats.recordingsReceived).toBe(0);
});
});
describe('Manual Match - Stats NOT Updated', () => {
let manualMatch;
let user1, user2;
let user1Token, user2Token;
beforeAll(async () => {
// Create two users for manual match
const hashedPassword = await hashPassword('TestPass123!');
user1 = await prisma.user.create({
data: {
username: 'stats_manual_user1',
email: 'stats_manual1@example.com',
passwordHash: hashedPassword,
emailVerified: true,
recordingsDone: 0,
recordingsReceived: 0,
},
});
user2 = await prisma.user.create({
data: {
username: 'stats_manual_user2',
email: 'stats_manual2@example.com',
passwordHash: hashedPassword,
emailVerified: true,
recordingsDone: 0,
recordingsReceived: 0,
},
});
user1Token = generateToken({ userId: user1.id });
user2Token = generateToken({ userId: user2.id });
// Add participants to event
await prisma.eventParticipant.createMany({
data: [
{ userId: user1.id, eventId: testEvent.id },
{ userId: user2.id, eventId: testEvent.id },
],
});
// Create manual match
const response = await request(app)
.post('/api/matches')
.set('Authorization', `Bearer ${user1Token}`)
.send({
targetUserId: user2.id,
eventSlug: testEvent.slug,
});
expect(response.status).toBe(201);
manualMatch = response.body.data;
});
afterAll(async () => {
if (user1 && user2) {
const userIds = [user1.id, user2.id];
await prisma.rating.deleteMany({ where: { raterId: { in: userIds } } });
await prisma.match.deleteMany({
where: {
OR: [
{ user1Id: { in: userIds } },
{ user2Id: { in: userIds } }
]
}
});
await prisma.eventParticipant.deleteMany({ where: { userId: { in: userIds } } });
await prisma.user.deleteMany({ where: { id: { in: userIds } } });
}
});
it('should NOT update stats for manual match after ratings', async () => {
// Accept match
await request(app)
.put(`/api/matches/${manualMatch.slug}/accept`)
.set('Authorization', `Bearer ${user2Token}`);
// Both users rate each other
await request(app)
.post(`/api/matches/${manualMatch.slug}/ratings`)
.set('Authorization', `Bearer ${user1Token}`)
.send({ score: 5 });
await request(app)
.post(`/api/matches/${manualMatch.slug}/ratings`)
.set('Authorization', `Bearer ${user2Token}`)
.send({ score: 5 });
// Verify match is completed
const match = await prisma.match.findUnique({
where: { slug: manualMatch.slug },
});
expect(match.status).toBe('completed');
expect(match.source).toBe('manual');
expect(match.statsApplied).toBe(true); // Flag set, but stats not updated
// Verify stats were NOT updated (source='manual')
const user1Stats = await prisma.user.findUnique({
where: { id: user1.id },
select: { recordingsDone: true, recordingsReceived: true },
});
const user2Stats = await prisma.user.findUnique({
where: { id: user2.id },
select: { recordingsDone: true, recordingsReceived: true },
});
expect(user1Stats.recordingsDone).toBe(0);
expect(user1Stats.recordingsReceived).toBe(0);
expect(user2Stats.recordingsDone).toBe(0);
expect(user2Stats.recordingsReceived).toBe(0);
});
});
});

View File

@@ -3,7 +3,7 @@ const { prisma } = require('../utils/db');
const { authenticate } = require('../middleware/auth'); const { authenticate } = require('../middleware/auth');
const { getIO } = require('../socket'); const { getIO } = require('../socket');
const matchingService = require('../services/matching'); const matchingService = require('../services/matching');
const { SUGGESTION_STATUS } = require('../constants'); const { SUGGESTION_STATUS, MATCH_STATUS } = require('../constants');
const router = express.Router(); const router = express.Router();
@@ -1260,21 +1260,77 @@ router.put('/:slug/match-suggestions/:suggestionId/status', authenticate, async
}); });
} }
// Update status // If accepted, create Match (if doesn't exist) and chat room
const updated = await prisma.recordingSuggestion.update({ if (status === 'accepted') {
where: { id: parseInt(suggestionId) }, const result = await prisma.$transaction(async (tx) => {
data: { status }, // Update suggestion status
select: { const updatedSuggestion = await tx.recordingSuggestion.update({
id: true, where: { id: parseInt(suggestionId) },
status: true, data: { status },
updatedAt: true, });
},
});
res.json({ // Check if Match already exists for this suggestion (idempotency)
success: true, const existingMatch = await tx.match.findUnique({
data: updated, where: { suggestionId: suggestion.id },
}); });
if (existingMatch) {
// Match already exists - just return the updated suggestion
return { suggestion: updatedSuggestion, match: existingMatch };
}
// Create private chat room
const chatRoom = await tx.chatRoom.create({
data: {
type: 'private',
eventId: event.id,
},
});
// Create Match with convention: user1 = dancer, user2 = recorder
const match = await tx.match.create({
data: {
user1Id: suggestion.heat.userId, // dancer
user2Id: suggestion.recorderId, // recorder
eventId: event.id,
suggestionId: suggestion.id,
source: 'auto',
status: MATCH_STATUS.ACCEPTED,
roomId: chatRoom.id,
statsApplied: false,
},
});
return { suggestion: updatedSuggestion, match };
});
res.json({
success: true,
data: {
id: result.suggestion.id,
status: result.suggestion.status,
updatedAt: result.suggestion.updatedAt,
matchId: result.match.id,
matchSlug: result.match.slug,
},
});
} else {
// Rejected - just update status
const updated = await prisma.recordingSuggestion.update({
where: { id: parseInt(suggestionId) },
data: { status },
select: {
id: true,
status: true,
updatedAt: true,
},
});
res.json({
success: true,
data: updated,
});
}
} catch (error) { } catch (error) {
next(error); next(error);
} }

View File

@@ -3,6 +3,7 @@ const { prisma } = require('../utils/db');
const { authenticate } = require('../middleware/auth'); const { authenticate } = require('../middleware/auth');
const { getIO } = require('../socket'); const { getIO } = require('../socket');
const { MATCH_STATUS } = require('../constants'); const { MATCH_STATUS } = require('../constants');
const matchingService = require('../services/matching');
const router = express.Router(); const router = express.Router();
@@ -826,10 +827,32 @@ router.post('/:slug/ratings', authenticate, async (req, res, next) => {
}); });
if (otherUserRating) { if (otherUserRating) {
// Both users have rated - mark match as completed // Both users have rated - mark match as completed and apply stats
// Get full match with required fields for stats update
const fullMatch = await prisma.match.findUnique({
where: { id: match.id },
select: {
id: true,
user1Id: true,
user2Id: true,
source: true,
statsApplied: true,
},
});
// Apply recording stats if not already applied (idempotency)
if (fullMatch && !fullMatch.statsApplied) {
await matchingService.applyRecordingStatsForMatch(fullMatch);
}
// Update match status to completed and mark stats as applied
await prisma.match.update({ await prisma.match.update({
where: { id: match.id }, where: { id: match.id },
data: { status: MATCH_STATUS.COMPLETED }, data: {
status: MATCH_STATUS.COMPLETED,
statsApplied: true,
},
}); });
} }

View File

@@ -544,10 +544,49 @@ async function getUserSuggestions(eventId, userId) {
}; };
} }
/**
* Apply recording stats update for a completed match
* Updates recordingsDone for recorder and recordingsReceived for dancer
*
* IMPORTANT: This function assumes the following convention for auto-matches:
* - user1Id = dancer (the one being recorded)
* - user2Id = recorder (the one doing the recording)
*
* Only applies to auto-matches (source='auto') to ensure stats reflect
* factual collaborations from the auto-matching system.
*
* @param {Object} match - Match object with user1Id, user2Id, and source
* @returns {Promise<void>}
*/
async function applyRecordingStatsForMatch(match) {
// Only apply stats for auto-matches
if (match.source !== 'auto') {
return;
}
// Convention: user1 = dancer, user2 = recorder
const dancerId = match.user1Id;
const recorderId = match.user2Id;
await prisma.$transaction([
// Increment recordingsDone for recorder
prisma.user.update({
where: { id: recorderId },
data: { recordingsDone: { increment: 1 } }
}),
// Increment recordingsReceived for dancer
prisma.user.update({
where: { id: dancerId },
data: { recordingsReceived: { increment: 1 } }
})
]);
}
module.exports = { module.exports = {
runMatching, runMatching,
saveMatchingResults, saveMatchingResults,
getUserSuggestions, getUserSuggestions,
applyRecordingStatsForMatch,
hasCollision, hasCollision,
getCoverableHeats, getCoverableHeats,
getTimeSlot, getTimeSlot,