diff --git a/src/services/player-service.js b/src/services/player-service.js index f73f7f8..eb5e4dd 100644 --- a/src/services/player-service.js +++ b/src/services/player-service.js @@ -4,6 +4,28 @@ const { fetchPlayerDataHTTP, parsePlayerData } = require('../scrapers/player-htt const { calculatePredictedRating } = require('./rating-calculator'); const logger = require('../logger'); +// Derives previous-month rating and the delta to it. Prefers PDGA's reported +// rating_change (canonical), falls back to our own monthly snapshots when +// rating_change is missing — common for players whose latest scrape failed. +function deriveMonthlyDeltas(rating, rawRatingChange, monthlyHistory) { + if (rating != null && rawRatingChange != null) { + return { lastMonthRating: rating - rawRatingChange, ratingChange: rawRatingChange }; + } + if (rating != null && monthlyHistory && monthlyHistory.length >= 1) { + // The "last month" snapshot depends on whether current_rating is already in + // history. If equal, current is the most recent entry — last month is the one + // before it. If not, current is newer than history — the latest entry IS last month. + const lastIdx = monthlyHistory.length - 1; + const lastMonth = (monthlyHistory[lastIdx] === rating) + ? (monthlyHistory.length >= 2 ? monthlyHistory[lastIdx - 1] : null) + : monthlyHistory[lastIdx]; + if (lastMonth != null) { + return { lastMonthRating: lastMonth, ratingChange: rating - lastMonth }; + } + } + return { lastMonthRating: null, ratingChange: rawRatingChange }; +} + async function getPlayerDataFromDB(pdgaNumber, { includeMonthlyHistory = true } = {}) { try { const cachedPlayer = await getPlayerFromDB(pdgaNumber); @@ -19,7 +41,7 @@ async function getPlayerDataFromDB(pdgaNumber, { includeMonthlyHistory = true } } const rating = cachedPlayer.current_rating; - const ratingChange = cachedPlayer.rating_change; + const rawRatingChange = cachedPlayer.rating_change; const resolvedPredicted = predictedRating > 0 ? predictedRating : null; const resolvedStdDev = stdDev > 0 ? stdDev : null; @@ -28,6 +50,8 @@ async function getPlayerDataFromDB(pdgaNumber, { includeMonthlyHistory = true } ? await getMonthlyHistory(cachedPlayer.pdga_number) : []; + const { lastMonthRating, ratingChange } = deriveMonthlyDeltas(rating, rawRatingChange, monthlyHistory); + return { pdgaNumber: cachedPlayer.pdga_number, name: cachedPlayer.name, @@ -35,8 +59,7 @@ async function getPlayerDataFromDB(pdgaNumber, { includeMonthlyHistory = true } ratingChange, predictedRating: resolvedPredicted, stdDev: resolvedStdDev, - // previous month's official rating (null when either value is missing) - lastMonthRating: (rating != null && ratingChange != null) ? rating - ratingChange : null, + lastMonthRating, // gap between next predicted update and current rating (null when either is missing) deltaPredicted: (resolvedPredicted != null && rating != null) ? resolvedPredicted - rating : null, monthlyHistory @@ -166,6 +189,10 @@ async function getAllRatingsFromDB(progressCallback = null) { if (playerData) { playerData.monthlyHistory = monthlyHistoryMap.get(pdgaNumber) ?? []; + // Re-derive now that history is attached — bulk path skipped includeMonthlyHistory + const derived = deriveMonthlyDeltas(playerData.rating, player.rating_change, playerData.monthlyHistory); + playerData.lastMonthRating = derived.lastMonthRating; + playerData.ratingChange = derived.ratingChange; ratings.push(playerData); }