1- import axios from 'axios' ;
21import { HistoryItem } from './storage' ;
32
43/**
54 * Football Data API match structure for type safety.
65 */
7- interface FootballDataMatch {
6+ export interface FootballDataMatch {
87 id : number ;
98 utcDate : string ;
109 status : string ;
@@ -51,7 +50,15 @@ export function extractMatchId(id: string): number | null {
5150 *
5251 * Note: Contributors adding new bet types should add cases here.
5352 */
54- function verifyPrediction (
53+ /**
54+ * verifyPrediction
55+ *
56+ * The core mathematical engine that compares a prediction against result data.
57+ * Handles diverse markets like 1X2, Over/Under, BTTS, and Half-time specific bets.
58+ *
59+ * Note: Contributors adding new bet types should add cases here.
60+ */
61+ export function verifyPrediction (
5562 prediction : string ,
5663 homeGoals : number ,
5764 awayGoals : number ,
@@ -120,153 +127,88 @@ function verifyPrediction(
120127}
121128
122129/**
123- * findMatchByTeams
124- *
125- * Fallback "Healer" logic. If a match ID is missing, searches the API for a match
126- * between the two teams on or near the predicted date.
130+ * Searches a provided list of matches for a fuzzy string match.
127131 */
128- async function findMatchByTeams (
132+ export function findMatchInList (
129133 homeTeam : string ,
130134 awayTeam : string ,
131- predictionDate : string | undefined , // The date we predicted the match for
132- apiKey : string
133- ) : Promise < number | null > {
134- if ( ! predictionDate ) return null ;
135-
136- try {
137- const todayStr = new Date ( ) . toISOString ( ) . split ( 'T' ) [ 0 ] ;
138- const matchDate = new Date ( predictionDate ) ;
139-
140- // Search from 3 days before predicted date up to today
141- const dateFrom = new Date ( matchDate ) ;
142- dateFrom . setDate ( matchDate . getDate ( ) - 3 ) ;
143-
144- const fromStr = dateFrom . toISOString ( ) . split ( 'T' ) [ 0 ] ;
145- const toStr = todayStr ; // Always search up to today to catch rescheduled matches
146-
147- const response = await axios . get ( `https://api.football-data.org/v4/matches` , {
148- headers : { 'X-Auth-Token' : apiKey } ,
149- params : { dateFrom : fromStr , dateTo : toStr }
150- } ) ;
151-
152- const matches = response . data . matches as FootballDataMatch [ ] ;
153- const totalFound = matches ?. length || 0 ;
154-
155- // DEBUG: Output all fixtures in the range for deep diagnostics
156- if ( totalFound > 0 ) {
157- const pool = matches . map ( m => `${ m . id } :${ m . homeTeam . name } vs ${ m . awayTeam . name } ` ) . join ( ' | ' ) ;
158- console . info ( `[Verifier] Searching ${ homeTeam } vs ${ awayTeam } in range ${ fromStr } to ${ toStr } . Pool (${ totalFound } matches): ${ pool } ` ) ;
159- }
160-
161- const normalize = ( n : string ) => n . toLowerCase ( )
162- . replace ( / \b ( f c | a f c | c f | s c | a c | u n i t e d | c i t y | r o v e r s | a l b i o n | t o w n | a t h l e t i c | c l u b e d e | c l u b | d e | a s | s s | s s c | b c | u c | u s | c d | c u b a | f u t e b o l | s a d | s p o r t s | s p o r t i n g | i n t e r n a t i o n a l | i n t e r n a z i o n a l e | i t a l y | p o r t u g a l | s p a i n | f r a n c e | e n g l a n d | g e r m a n y ) \b / g, '' )
135+ matches : FootballDataMatch [ ]
136+ ) : FootballDataMatch | null {
137+ const normalize = ( n : string ) => {
138+ if ( ! n ) return '' ;
139+ return n . toLowerCase ( )
140+ . replace ( / \b ( f c | a f c | c f | s c | a c | r o v e r s | a l b i o n | t o w n | a t h l e t i c | c l u b e d e | c l u b | d e | a s | s s | s s c | b c | u c | u s | c d | c u b a | f u t e b o l | s a d | s p o r t s | s p o r t i n g | i n t e r n a t i o n a l | i n t e r n a z i o n a l e | i t a l y | p o r t u g a l | s p a i n | f r a n c e | e n g l a n d | g e r m a n y ) \b / g, '' )
163141 . replace ( / [ \W _ ] + / g, ' ' )
164142 . trim ( ) ;
143+ } ;
165144
166- // Technical Mappings
167- const nicknames : Record < string , string [ ] > = {
168- 'wolves' : [ 'wolverhampton' ] ,
169- 'inter' : [ 'internazionale' ] ,
170- 'mancity' : [ 'manchester city' ] ,
171- 'manutd' : [ 'manchester united' ] ,
172- 'avs' : [ 'avs futebol sad' ] ,
173- 'porto' : [ 'fc porto' , 'futebol clube do porto' ]
174- } ;
175-
176- const getSearchTerms = ( name : string ) => {
177- const n = normalize ( name ) ;
178- const terms = new Set ( [ n ] ) ;
179- // Extract key words (e.g. "Porto" from "FC Porto")
180- n . split ( ' ' ) . forEach ( w => { if ( w . length > 3 ) terms . add ( w ) ; } ) ;
181- Object . entries ( nicknames ) . forEach ( ( [ key , val ] ) => {
182- if ( n . includes ( key ) || key . includes ( n ) ) val . forEach ( v => terms . add ( v ) ) ;
183- } ) ;
184- return Array . from ( terms ) ;
185- } ;
186-
187- const targetHTerms = getSearchTerms ( homeTeam ) ;
188- const targetATerms = getSearchTerms ( awayTeam ) ;
145+ const nicknames : Record < string , string [ ] > = {
146+ 'wolves' : [ 'wolverhampton' , 'wolverhampton wanderers' ] ,
147+ 'inter' : [ 'internazionale' , 'inter milan' , 'internazionale milano' ] ,
148+ 'mancity' : [ 'manchester city' ] ,
149+ 'manutd' : [ 'manchester united' , 'man utd' ] ,
150+ 'spurs' : [ 'tottenham' , 'tottenham hotspur' ] ,
151+ 'avs' : [ 'avs futebol sad' ] ,
152+ 'porto' : [ 'fc porto' , 'futebol clube do porto' ] ,
153+ 'benfica' : [ 'sl benfica' , 'sport lisboa e benfica' ] ,
154+ 'milan' : [ 'ac milan' ] ,
155+ 'verona' : [ 'hellas verona' ]
156+ } ;
189157
190- const match = matches . find ( m => {
191- const h = normalize ( m . homeTeam . name ) ;
192- const a = normalize ( m . awayTeam . name ) ;
158+ const getSearchTerms = ( name : string ) => {
159+ const n = normalize ( name ) ;
160+ const terms = new Set ( [ n ] ) ;
161+ // Extract key words (e.g. "Porto" from "FC Porto")
162+ n . split ( ' ' ) . forEach ( w => { if ( w . length > 3 ) terms . add ( w ) ; } ) ;
163+ Object . entries ( nicknames ) . forEach ( ( [ key , val ] ) => {
164+ if ( n . includes ( key ) || key . includes ( n ) ) val . forEach ( v => terms . add ( v ) ) ;
165+ } ) ;
166+ return Array . from ( terms ) ;
167+ } ;
193168
194- // Stricter matching: ONE team from prediction MUST match Home API,
195- // AND the OTHER team from prediction MUST match Away API.
196- const homeMatchesH = targetHTerms . some ( t => h . includes ( t ) || t . includes ( h ) ) ;
197- const homeMatchesA = targetHTerms . some ( t => a . includes ( t ) || t . includes ( a ) ) ;
169+ const targetHTerms = getSearchTerms ( homeTeam ) ;
170+ const targetATerms = getSearchTerms ( awayTeam ) ;
198171
199- const awayMatchesH = targetATerms . some ( t => h . includes ( t ) || t . includes ( h ) ) ;
200- const awayMatchesA = targetATerms . some ( t => a . includes ( t ) || t . includes ( a ) ) ;
172+ return matches . find ( m => {
173+ const h = normalize ( m . homeTeam . name ) ;
174+ const a = normalize ( m . awayTeam . name ) ;
201175
202- const normalOrder = homeMatchesH && awayMatchesA ;
203- const swappedOrder = homeMatchesA && awayMatchesH ;
176+ const homeMatchesH = targetHTerms . some ( t => h . includes ( t ) || t . includes ( h ) ) ;
177+ const homeMatchesA = targetHTerms . some ( t => a . includes ( t ) || t . includes ( a ) ) ;
178+ const awayMatchesH = targetATerms . some ( t => h . includes ( t ) || t . includes ( h ) ) ;
179+ const awayMatchesA = targetATerms . some ( t => a . includes ( t ) || t . includes ( a ) ) ;
204180
205- return normalOrder || swappedOrder ;
206- } ) ;
181+ const normalOrder = homeMatchesH && awayMatchesA ;
182+ const swappedOrder = homeMatchesA && awayMatchesH ;
207183
208- if ( match ) {
209- console . info ( `[Verifier] SUCCESS: Found ${ match . homeTeam . name } vs ${ match . awayTeam . name } for ${ homeTeam } vs ${ awayTeam } ` ) ;
210- return match . id ;
211- } else {
212- console . warn ( `[Verifier] FAILED: No match found for ${ homeTeam } vs ${ awayTeam } ` ) ;
213- return null ;
214- }
215- } catch ( e : any ) {
216- if ( e . response ?. status === 429 ) throw e ;
217- console . error ( "[Verifier] API Error during search:" , homeTeam , "vs" , awayTeam ) ;
218- return null ;
219- }
184+ return normalOrder || swappedOrder ;
185+ } ) || null ;
220186}
221187
222188/**
223- * verifyMatch
224- *
225- * Main orchestration for match verification. Retrieves match status/results and
226- * settles predictions.
189+ * Verifies a prediction using a provided Match object (avoids API calls)
227190 */
228- export async function verifyMatch ( item : HistoryItem , apiKey : string , date ?: string ) : Promise < HistoryItem > {
229- let matchId : number | null = item . matchId || extractMatchId ( item . id || '' ) ;
230-
231- // Trigger Healer if extraction fails (older "local" IDs)
232- if ( ! matchId ) {
233- matchId = await findMatchByTeams ( item . homeTeam , item . awayTeam , date , apiKey ) ;
191+ export function verifyMatchFromData ( item : HistoryItem , match : FootballDataMatch ) : HistoryItem {
192+ if ( match . status !== 'FINISHED' && match . status !== 'IN_PLAY' && match . status !== 'PAUSED' ) {
193+ return { ...item , result : 'Pending' , matchId : match . id } ;
234194 }
235195
236- if ( ! matchId ) return { ...item , result : 'Pending' } ;
237-
238- try {
239- const response = await axios . get ( `https://api.football-data.org/v4/matches/${ matchId } ` , {
240- headers : { 'X-Auth-Token' : apiKey }
241- } ) ;
242-
243- const match = response . data as FootballDataMatch ;
244-
245- // Skip verification if match hasn't started or isn't finished enough to be certain
246- if ( match . status !== 'FINISHED' && match . status !== 'IN_PLAY' && match . status !== 'PAUSED' ) {
247- return { ...item , result : 'Pending' , matchId } ;
248- }
249-
250- const { home, away } = match . score . fullTime ;
251- if ( home === null || away === null ) return { ...item , result : 'Pending' , matchId } ;
252-
253- const result = verifyPrediction (
254- item . prediction ,
255- home ,
256- away ,
257- match . score . halfTime . home ,
258- match . score . halfTime . away
259- ) ;
260-
261- return {
262- ...item ,
263- result : match . status === 'FINISHED' ? result : 'Pending' ,
264- score : formatScore ( match ) ,
265- matchId
266- } ;
196+ const { home, away } = match . score . fullTime ;
197+ if ( home === null || away === null ) return { ...item , result : 'Pending' , matchId : match . id } ;
198+
199+ const result = verifyPrediction (
200+ item . prediction ,
201+ home ,
202+ away ,
203+ match . score . halfTime . home ,
204+ match . score . halfTime . away
205+ ) ;
206+
207+ return {
208+ ...item ,
209+ result : match . status === 'FINISHED' ? result : 'Pending' ,
210+ score : formatScore ( match ) ,
211+ matchId : match . id
212+ } ;
213+ }
267214
268- } catch ( error : any ) {
269- if ( error . response ?. status === 429 ) throw error ;
270- return { ...item , matchId } ;
271- }
272- }
0 commit comments