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

@@ -3,7 +3,7 @@ const { prisma } = require('../utils/db');
const { authenticate } = require('../middleware/auth');
const { getIO } = require('../socket');
const matchingService = require('../services/matching');
const { SUGGESTION_STATUS } = require('../constants');
const { SUGGESTION_STATUS, MATCH_STATUS } = require('../constants');
const router = express.Router();
@@ -1260,21 +1260,77 @@ router.put('/:slug/match-suggestions/:suggestionId/status', authenticate, async
});
}
// Update status
const updated = await prisma.recordingSuggestion.update({
where: { id: parseInt(suggestionId) },
data: { status },
select: {
id: true,
status: true,
updatedAt: true,
},
});
// If accepted, create Match (if doesn't exist) and chat room
if (status === 'accepted') {
const result = await prisma.$transaction(async (tx) => {
// Update suggestion status
const updatedSuggestion = await tx.recordingSuggestion.update({
where: { id: parseInt(suggestionId) },
data: { status },
});
res.json({
success: true,
data: updated,
});
// Check if Match already exists for this suggestion (idempotency)
const existingMatch = await tx.match.findUnique({
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) {
next(error);
}