diff --git a/src/client/ghin/models/scores/score.ts b/src/client/ghin/models/scores/score.ts index ecbacd4..18d3c1b 100644 --- a/src/client/ghin/models/scores/score.ts +++ b/src/client/ghin/models/scores/score.ts @@ -31,16 +31,17 @@ const schemaScoreTypeWithTransform: z.ZodType scoreTypesMap[value] ) -const scoreStatuses = ['VALIDATED', 'UNDER_REVIEW'] as const +const scoreStatuses = ['VALIDATED', 'UNDER_REVIEW', 'TEMPORARY'] as const const schemaScoreStatus = z.enum(scoreStatuses) type ScoreStatus = z.infer -const rawScoreStatuses = ['Validated', 'UnderReview'] as const +const rawScoreStatuses = ['Validated', 'UnderReview', 'Temporary'] as const const schemaRawScoreStatus = z.enum(rawScoreStatuses) const scoreStatusesMap = { Validated: 'VALIDATED', UnderReview: 'UNDER_REVIEW', + Temporary: 'TEMPORARY', } as const const schemaScoreStatusWithTransform = schemaRawScoreStatus.transform( diff --git a/src/client/ghin/models/scores/statistics.ts b/src/client/ghin/models/scores/statistics.ts index 9aeb07b..d60abd0 100644 --- a/src/client/ghin/models/scores/statistics.ts +++ b/src/client/ghin/models/scores/statistics.ts @@ -1,23 +1,25 @@ import { z } from 'zod' -import { date, emptyStringToNull, float, number } from '../../../../models' +import { date, emptyStringToNull, float, floatSafe, number } from '../../../../models' +// GHIN sends NaN (or "NaN" strings) for fairway/miss percents on rounds where +// the user didn't log that stat. Use floatSafe (nullable) for those fields. const schemaStatistics = z.object({ birdies_or_better_percent: float, bogeys_percent: float, double_bogeys_percent: float, - fairway_hits_percent: float, + fairway_hits_percent: floatSafe, gir_percent: float, last_stats_update_date: date, last_stats_update_type: emptyStringToNull, missed_general_approach_shot_accuracy_percent: float, missed_left_approach_shot_accuracy_percent: float, - missed_left_percent: float, + missed_left_percent: floatSafe, missed_long_approach_shot_accuracy_percent: float, - missed_long_percent: float, + missed_long_percent: floatSafe, missed_right_approach_shot_accuracy_percent: float, - missed_right_percent: float, + missed_right_percent: floatSafe, missed_short_approach_shot_accuracy_percent: float, - missed_short_percent: float, + missed_short_percent: floatSafe, one_putt_or_better_percent: float, par3s_average: float, par4s_average: float, diff --git a/src/models/validation.ts b/src/models/validation.ts index 980ad1b..f72126d 100644 --- a/src/models/validation.ts +++ b/src/models/validation.ts @@ -14,6 +14,18 @@ const date = z const emptyString = z.string().trim() const emptyStringToNull = emptyString.nullable().transform((value) => value || null) const float = z.coerce.number() +// NaN-tolerant float: coerces NaN / "NaN" / empty string to null. GHIN returns +// these in statistics objects for rounds without tracked data. +const floatSafe = z.preprocess((v) => { + if (v === null || v === undefined) return null + if (typeof v === 'string') { + if (v === '' || v.toLowerCase() === 'nan') return null + const n = Number(v) + return Number.isFinite(n) ? n : null + } + if (typeof v === 'number') return Number.isFinite(v) ? v : null + return v +}, z.number().nullable()) const gender = z.enum(['M', 'F']) const handicap = z @@ -65,4 +77,4 @@ const shortDate = z return new Date(`${year}-${month}-${day}T00:00Z`) }) -export { boolean, date, emptyStringToNull, float, gender, handicap, monthDay, number, shortDate, string } +export { boolean, date, emptyStringToNull, float, floatSafe, gender, handicap, monthDay, number, shortDate, string }