overtime-live-trading-utils 2.1.43 → 2.1.45-rc.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/.circleci/config.yml +32 -32
  2. package/.prettierrc +9 -9
  3. package/codecov.yml +20 -20
  4. package/index.ts +26 -26
  5. package/jest.config.ts +16 -16
  6. package/main.js +1 -1
  7. package/package.json +30 -30
  8. package/src/constants/common.ts +8 -7
  9. package/src/constants/errors.ts +7 -6
  10. package/src/constants/sports.ts +78 -78
  11. package/src/enums/sports.ts +109 -109
  12. package/src/tests/mock/MockLeagueMap.ts +200 -170
  13. package/src/tests/mock/MockOpticOddsEvents.ts +662 -662
  14. package/src/tests/mock/MockOpticSoccer.ts +9864 -9378
  15. package/src/tests/mock/MockSoccerRedis.ts +2308 -2308
  16. package/src/tests/unit/bookmakers.test.ts +149 -79
  17. package/src/tests/unit/markets.test.ts +177 -156
  18. package/src/tests/unit/odds.test.ts +104 -92
  19. package/src/tests/unit/resolution.test.ts +1489 -1489
  20. package/src/tests/unit/sports.test.ts +58 -58
  21. package/src/tests/unit/spread.test.ts +145 -131
  22. package/src/tests/utils/helper.ts +10 -0
  23. package/src/types/bookmakers.ts +7 -0
  24. package/src/types/missing-types.d.ts +2 -2
  25. package/src/types/odds.ts +80 -61
  26. package/src/types/resolution.ts +656 -656
  27. package/src/types/sports.ts +22 -19
  28. package/src/utils/bookmakers.ts +322 -159
  29. package/src/utils/constraints.ts +210 -210
  30. package/src/utils/gameMatching.ts +81 -81
  31. package/src/utils/markets.ts +120 -119
  32. package/src/utils/odds.ts +953 -918
  33. package/src/utils/opticOdds.ts +71 -71
  34. package/src/utils/resolution.ts +319 -319
  35. package/src/utils/sportPeriodMapping.ts +36 -36
  36. package/src/utils/sports.ts +51 -51
  37. package/src/utils/spread.ts +97 -97
  38. package/tsconfig.json +16 -16
  39. package/webpack.config.js +24 -24
  40. package/CLAUDE.md +0 -84
  41. package/resolution_live_markets.md +0 -356
@@ -1,1489 +1,1489 @@
1
- import {
2
- detectCompletedPeriods,
3
- canResolveMarketsForEvent,
4
- canResolveMultipleTypeIdsForEvent,
5
- filterMarketsThatCanBeResolved,
6
- } from '../../utils/resolution';
7
- import { SportPeriodType } from '../../types/resolution';
8
- import {
9
- MockSoccerCompletedEvent,
10
- MockSoccerLiveSecondHalf,
11
- MockSoccerLiveFirstHalf,
12
- MockNFLCompletedEvent,
13
- MockNFLLiveThirdQuarter,
14
- MockMLBCompletedEvent,
15
- MockMLBLiveSixthInning,
16
- MockSoccerLiveFirstHalfInProgress,
17
- MockNFLCompletedWithOvertime,
18
- MockNBACompletedEvent,
19
- MockNBALiveAtHalftime,
20
- } from '../mock/MockOpticOddsEvents';
21
-
22
- describe('Resolution Utils', () => {
23
- describe('detectCompletedPeriods', () => {
24
- it('Should detect completed periods for real completed soccer game (UEFA Europa League)', () => {
25
- const result = detectCompletedPeriods(MockSoccerCompletedEvent);
26
-
27
- expect(result).not.toBeNull();
28
- expect(result?.completedPeriods).toEqual([1, 2]);
29
- expect(result?.readyForResolution).toBe(true);
30
- expect(result?.periodScores['period1']).toEqual({ home: 1.0, away: 0.0 });
31
- expect(result?.periodScores['period2']).toEqual({ home: 1.0, away: 0.0 });
32
- });
33
-
34
- it('Should detect completed first half in real live soccer game (2nd half)', () => {
35
- const result = detectCompletedPeriods(MockSoccerLiveSecondHalf);
36
-
37
- expect(result).not.toBeNull();
38
- expect(result?.completedPeriods).toEqual([1]);
39
- expect(result?.readyForResolution).toBe(true);
40
- expect(result?.periodScores['period1']).toEqual({ home: 1.0, away: 1.0 });
41
- expect(result?.currentPeriod).toBe(2);
42
- });
43
-
44
- it('Should return null for real live soccer game in first half (no completed periods)', () => {
45
- const result = detectCompletedPeriods(MockSoccerLiveFirstHalf);
46
-
47
- expect(result).toBeNull();
48
- });
49
-
50
- it('Should detect completed periods for real completed NFL game (Patriots vs Panthers)', () => {
51
- const result = detectCompletedPeriods(MockNFLCompletedEvent);
52
-
53
- expect(result).not.toBeNull();
54
- expect(result?.completedPeriods).toEqual([1, 2, 3, 4]);
55
- expect(result?.readyForResolution).toBe(true);
56
- expect(result?.periodScores['period1']).toEqual({ home: 7.0, away: 6.0 });
57
- expect(result?.periodScores['period2']).toEqual({ home: 21.0, away: 0.0 });
58
- expect(result?.periodScores['period3']).toEqual({ home: 7.0, away: 0.0 });
59
- expect(result?.periodScores['period4']).toEqual({ home: 7.0, away: 7.0 });
60
- });
61
-
62
- it('Should detect completed quarters in real live NFL game (3rd quarter)', () => {
63
- const result = detectCompletedPeriods(MockNFLLiveThirdQuarter);
64
-
65
- expect(result).not.toBeNull();
66
- expect(result?.completedPeriods).toEqual([1, 2]);
67
- expect(result?.readyForResolution).toBe(true);
68
- expect(result?.periodScores['period1']).toEqual({ home: 7.0, away: 3.0 });
69
- expect(result?.periodScores['period2']).toEqual({ home: 14.0, away: 7.0 });
70
- expect(result?.currentPeriod).toBe(3);
71
- });
72
-
73
- it('Should detect completed innings for real completed MLB game (Tigers vs Guardians)', () => {
74
- const result = detectCompletedPeriods(MockMLBCompletedEvent);
75
-
76
- expect(result).not.toBeNull();
77
- expect(result?.completedPeriods).toEqual([1, 2, 3, 4, 5, 6, 7, 8, 9]);
78
- expect(result?.readyForResolution).toBe(true);
79
- expect(result?.periodScores['period1']).toEqual({ home: 0.0, away: 0.0 });
80
- expect(result?.periodScores['period3']).toEqual({ home: 0.0, away: 1.0 });
81
- expect(result?.periodScores['period7']).toEqual({ home: 0.0, away: 4.0 });
82
- expect(result?.periodScores['period8']).toEqual({ home: 2.0, away: 0.0 });
83
- expect(result?.periodScores['period9']).toEqual({ home: 0.0, away: 0.0 });
84
- });
85
-
86
- it('Should detect completed innings in real live MLB game (6th inning)', () => {
87
- const result = detectCompletedPeriods(MockMLBLiveSixthInning);
88
-
89
- expect(result).not.toBeNull();
90
- expect(result?.completedPeriods).toEqual([1, 2, 3, 4, 5]);
91
- expect(result?.readyForResolution).toBe(true);
92
- expect(result?.periodScores['period1']).toEqual({ home: 0.0, away: 1.0 });
93
- expect(result?.periodScores['period2']).toEqual({ home: 2.0, away: 0.0 });
94
- expect(result?.periodScores['period3']).toEqual({ home: 1.0, away: 0.0 });
95
- expect(result?.periodScores['period4']).toEqual({ home: 0.0, away: 0.0 });
96
- expect(result?.periodScores['period5']).toEqual({ home: 1.0, away: 1.0 });
97
- expect(result?.currentPeriod).toBe(6);
98
- });
99
-
100
- it('Should detect completed periods for real completed NBA game (Warriors vs Suns)', () => {
101
- const result = detectCompletedPeriods(MockNBACompletedEvent);
102
-
103
- expect(result).not.toBeNull();
104
- expect(result?.completedPeriods).toEqual([1, 2, 3, 4]);
105
- expect(result?.readyForResolution).toBe(true);
106
- expect(result?.periodScores['period1']).toEqual({ home: 33.0, away: 19.0 });
107
- expect(result?.periodScores['period2']).toEqual({ home: 35.0, away: 30.0 });
108
- expect(result?.periodScores['period3']).toEqual({ home: 24.0, away: 34.0 });
109
- expect(result?.periodScores['period4']).toEqual({ home: 26.0, away: 24.0 });
110
- });
111
-
112
- it('Should detect completed quarters at halftime for real NBA game (Warriors vs Suns)', () => {
113
- const result = detectCompletedPeriods(MockNBALiveAtHalftime);
114
-
115
- expect(result).not.toBeNull();
116
- expect(result?.completedPeriods).toEqual([1, 2]); // Both quarters complete at halftime
117
- expect(result?.readyForResolution).toBe(true);
118
- expect(result?.periodScores['period1']).toEqual({ home: 33.0, away: 19.0 });
119
- expect(result?.periodScores['period2']).toEqual({ home: 35.0, away: 30.0 });
120
- expect(result?.currentPeriod).toBe(2); // Highest period with data at halftime
121
- });
122
-
123
- it('Should return null for real live soccer game with non-numeric period indicator (1H)', () => {
124
- const result = detectCompletedPeriods(MockSoccerLiveFirstHalfInProgress);
125
-
126
- // Period 1 exists in data but period is "1H" (non-numeric) meaning first half is still in progress
127
- // Period 1 is NOT complete until we see period_2 in the data or status becomes completed
128
- expect(result).toBeNull();
129
- });
130
-
131
- it('Should detect completed periods for completed American Football game', () => {
132
- const event = {
133
- sport: {
134
- id: 'football',
135
- name: 'Football',
136
- },
137
- fixture: {
138
- id: '20250930BEED03AA',
139
- status: 'completed',
140
- is_live: false,
141
- },
142
- scores: {
143
- home: {
144
- total: 28.0,
145
- periods: {
146
- period_1: 7.0,
147
- period_2: 14.0,
148
- period_3: 0.0,
149
- period_4: 7.0,
150
- },
151
- },
152
- away: {
153
- total: 3.0,
154
- periods: {
155
- period_1: 3.0,
156
- period_2: 0.0,
157
- period_3: 0.0,
158
- period_4: 0.0,
159
- },
160
- },
161
- },
162
- in_play: {
163
- period: '4',
164
- },
165
- };
166
-
167
- const result = detectCompletedPeriods(event);
168
-
169
- expect(result).not.toBeNull();
170
- expect(result?.completedPeriods).toEqual([1, 2, 3, 4]);
171
- expect(result?.readyForResolution).toBe(true);
172
- expect(result?.periodScores['period1']).toEqual({ home: 7.0, away: 3.0 });
173
- expect(result?.periodScores['period2']).toEqual({ home: 14.0, away: 0.0 });
174
- expect(result?.periodScores['period3']).toEqual({ home: 0.0, away: 0.0 });
175
- expect(result?.periodScores['period4']).toEqual({ home: 7.0, away: 0.0 });
176
- });
177
-
178
- it('Should detect completed quarters in live American Football game', () => {
179
- const event = {
180
- sport: {
181
- name: 'Football',
182
- },
183
- fixture: {
184
- id: 'nfl-live-123',
185
- status: 'live',
186
- is_live: true,
187
- },
188
- scores: {
189
- home: {
190
- total: 21.0,
191
- periods: {
192
- period_1: 7.0,
193
- period_2: 14.0,
194
- period_3: 0.0,
195
- },
196
- },
197
- away: {
198
- total: 10.0,
199
- periods: {
200
- period_1: 3.0,
201
- period_2: 7.0,
202
- period_3: 0.0,
203
- },
204
- },
205
- },
206
- in_play: {
207
- period: '3',
208
- },
209
- };
210
-
211
- const result = detectCompletedPeriods(event);
212
-
213
- expect(result).not.toBeNull();
214
- expect(result?.completedPeriods).toEqual([1, 2]);
215
- expect(result?.readyForResolution).toBe(true);
216
- expect(result?.periodScores['period1']).toEqual({ home: 7.0, away: 3.0 });
217
- expect(result?.periodScores['period2']).toEqual({ home: 14.0, away: 7.0 });
218
- expect(result?.currentPeriod).toBe(3);
219
- });
220
-
221
- it('Should detect all quarters complete in American Football overtime', () => {
222
- const event = {
223
- sport: {
224
- name: 'American Football',
225
- },
226
- fixture: {
227
- id: 'nfl-ot-456',
228
- status: 'live',
229
- is_live: true,
230
- },
231
- scores: {
232
- home: {
233
- total: 24.0,
234
- periods: {
235
- period_1: 7.0,
236
- period_2: 7.0,
237
- period_3: 3.0,
238
- period_4: 7.0,
239
- },
240
- },
241
- away: {
242
- total: 24.0,
243
- periods: {
244
- period_1: 10.0,
245
- period_2: 7.0,
246
- period_3: 0.0,
247
- period_4: 7.0,
248
- },
249
- },
250
- },
251
- in_play: {
252
- period: 'overtime',
253
- },
254
- };
255
-
256
- const result = detectCompletedPeriods(event);
257
-
258
- expect(result).not.toBeNull();
259
- expect(result?.completedPeriods).toEqual([1, 2, 3, 4]);
260
- expect(result?.readyForResolution).toBe(true);
261
- });
262
-
263
- it('Should detect completed first half in live soccer game', () => {
264
- const event = {
265
- sport: {
266
- name: 'Soccer',
267
- },
268
- fixture: {
269
- id: 'soccer-123',
270
- status: 'live',
271
- is_live: true,
272
- },
273
- scores: {
274
- home: {
275
- total: 2.0,
276
- periods: {
277
- period_1: 1.0,
278
- },
279
- },
280
- away: {
281
- total: 1.0,
282
- periods: {
283
- period_1: 0.0,
284
- },
285
- },
286
- },
287
- in_play: {
288
- period: '2',
289
- },
290
- };
291
-
292
- const result = detectCompletedPeriods(event);
293
-
294
- expect(result).not.toBeNull();
295
- expect(result?.completedPeriods).toEqual([1]);
296
- expect(result?.readyForResolution).toBe(true);
297
- expect(result?.periodScores['period1']).toEqual({ home: 1.0, away: 0.0 });
298
- });
299
-
300
- it('Should detect completed quarters in live basketball game', () => {
301
- const event = {
302
- sport: {
303
- name: 'Basketball',
304
- },
305
- fixture: {
306
- id: 'nba-789',
307
- status: 'live',
308
- is_live: true,
309
- },
310
- scores: {
311
- home: {
312
- total: 65.0,
313
- periods: {
314
- period_1: 25.0,
315
- period_2: 20.0,
316
- },
317
- },
318
- away: {
319
- total: 62.0,
320
- periods: {
321
- period_1: 22.0,
322
- period_2: 18.0,
323
- },
324
- },
325
- },
326
- in_play: {
327
- period: '3',
328
- },
329
- };
330
-
331
- const result = detectCompletedPeriods(event);
332
-
333
- expect(result).not.toBeNull();
334
- expect(result?.completedPeriods).toEqual([1, 2]);
335
- expect(result?.readyForResolution).toBe(true);
336
- expect(result?.periodScores['period1']).toEqual({ home: 25.0, away: 22.0 });
337
- expect(result?.periodScores['period2']).toEqual({ home: 20.0, away: 18.0 });
338
- });
339
-
340
- it('Should detect completed sets in live tennis match', () => {
341
- const event = {
342
- sport: {
343
- name: 'Tennis',
344
- },
345
- fixture: {
346
- id: 'tennis-101',
347
- status: 'live',
348
- is_live: true,
349
- },
350
- scores: {
351
- home: {
352
- total: 1.0,
353
- periods: {
354
- period_1: 6.0,
355
- period_2: 3.0,
356
- },
357
- },
358
- away: {
359
- total: 1.0,
360
- periods: {
361
- period_1: 4.0,
362
- period_2: 6.0,
363
- },
364
- },
365
- },
366
- in_play: {
367
- period: '3',
368
- },
369
- };
370
-
371
- const result = detectCompletedPeriods(event);
372
-
373
- expect(result).not.toBeNull();
374
- expect(result?.completedPeriods).toEqual([1, 2]);
375
- expect(result?.readyForResolution).toBe(true);
376
- expect(result?.periodScores['period1']).toEqual({ home: 6.0, away: 4.0 });
377
- expect(result?.periodScores['period2']).toEqual({ home: 3.0, away: 6.0 });
378
- });
379
-
380
- it('Should detect completed periods in live hockey game', () => {
381
- const event = {
382
- sport: {
383
- name: 'Ice Hockey',
384
- },
385
- fixture: {
386
- id: 'nhl-202',
387
- status: 'live',
388
- is_live: true,
389
- },
390
- scores: {
391
- home: {
392
- total: 2.0,
393
- periods: {
394
- period_1: 1.0,
395
- },
396
- },
397
- away: {
398
- total: 1.0,
399
- periods: {
400
- period_1: 1.0,
401
- },
402
- },
403
- },
404
- in_play: {
405
- period: '2',
406
- },
407
- };
408
-
409
- const result = detectCompletedPeriods(event);
410
-
411
- expect(result).not.toBeNull();
412
- expect(result?.completedPeriods).toEqual([1]);
413
- expect(result?.readyForResolution).toBe(true);
414
- expect(result?.periodScores['period1']).toEqual({ home: 1.0, away: 1.0 });
415
- });
416
-
417
- it('Should detect all periods complete in hockey overtime', () => {
418
- const event = {
419
- sport: {
420
- name: 'Hockey',
421
- },
422
- fixture: {
423
- id: 'nhl-303',
424
- status: 'live',
425
- is_live: true,
426
- },
427
- scores: {
428
- home: {
429
- total: 3.0,
430
- periods: {
431
- period_1: 1.0,
432
- period_2: 1.0,
433
- period_3: 1.0,
434
- },
435
- },
436
- away: {
437
- total: 3.0,
438
- periods: {
439
- period_1: 0.0,
440
- period_2: 2.0,
441
- period_3: 1.0,
442
- },
443
- },
444
- },
445
- in_play: {
446
- period: 'overtime',
447
- },
448
- };
449
-
450
- const result = detectCompletedPeriods(event);
451
-
452
- expect(result).not.toBeNull();
453
- expect(result?.completedPeriods).toEqual([1, 2, 3]);
454
- expect(result?.readyForResolution).toBe(true);
455
- });
456
-
457
- it('Should detect completed innings in live baseball game', () => {
458
- const event = {
459
- sport: {
460
- name: 'Baseball',
461
- },
462
- fixture: {
463
- id: 'mlb-404',
464
- status: 'live',
465
- is_live: true,
466
- },
467
- scores: {
468
- home: {
469
- total: 4.0,
470
- periods: {
471
- period_1: 1.0,
472
- period_2: 0.0,
473
- period_3: 2.0,
474
- period_4: 1.0,
475
- },
476
- },
477
- away: {
478
- total: 3.0,
479
- periods: {
480
- period_1: 0.0,
481
- period_2: 1.0,
482
- period_3: 1.0,
483
- period_4: 1.0,
484
- },
485
- },
486
- },
487
- in_play: {
488
- period: '5',
489
- },
490
- };
491
-
492
- const result = detectCompletedPeriods(event);
493
-
494
- expect(result).not.toBeNull();
495
- expect(result?.completedPeriods).toEqual([1, 2, 3, 4]);
496
- expect(result?.readyForResolution).toBe(true);
497
- });
498
-
499
- it('Should detect completed sets in live volleyball match', () => {
500
- const event = {
501
- sport: {
502
- name: 'Volleyball',
503
- },
504
- fixture: {
505
- id: 'volleyball-505',
506
- status: 'live',
507
- is_live: true,
508
- },
509
- scores: {
510
- home: {
511
- total: 1.0,
512
- periods: {
513
- period_1: 25.0,
514
- period_2: 22.0,
515
- },
516
- },
517
- away: {
518
- total: 1.0,
519
- periods: {
520
- period_1: 23.0,
521
- period_2: 25.0,
522
- },
523
- },
524
- },
525
- in_play: {
526
- period: '3',
527
- },
528
- };
529
-
530
- const result = detectCompletedPeriods(event);
531
-
532
- expect(result).not.toBeNull();
533
- expect(result?.completedPeriods).toEqual([1, 2]);
534
- expect(result?.readyForResolution).toBe(true);
535
- });
536
-
537
- it('Should return null for game not started', () => {
538
- const event = {
539
- sport: {
540
- name: 'Soccer',
541
- },
542
- fixture: {
543
- id: 'future-game',
544
- status: 'scheduled',
545
- is_live: false,
546
- },
547
- scores: {
548
- home: {
549
- total: 0.0,
550
- periods: {},
551
- },
552
- away: {
553
- total: 0.0,
554
- periods: {},
555
- },
556
- },
557
- in_play: {
558
- period: null,
559
- },
560
- };
561
-
562
- const result = detectCompletedPeriods(event);
563
-
564
- expect(result).toBeNull();
565
- });
566
-
567
- it('Should return null for game in first period with no completed periods', () => {
568
- const event = {
569
- sport: {
570
- name: 'Basketball',
571
- },
572
- fixture: {
573
- id: 'early-game',
574
- status: 'live',
575
- is_live: true,
576
- },
577
- scores: {
578
- home: {
579
- total: 12.0,
580
- periods: {},
581
- },
582
- away: {
583
- total: 10.0,
584
- periods: {},
585
- },
586
- },
587
- in_play: {
588
- period: '1',
589
- },
590
- };
591
-
592
- const result = detectCompletedPeriods(event);
593
-
594
- expect(result).toBeNull();
595
- });
596
-
597
- it('Bug Fix: Should NOT mark current period as complete when future periods have data (live in period 3)', () => {
598
- // This tests the bug where OpticOdds API includes future period data with scores (including zeros)
599
- // Period 3 is currently being played, period 4 has 0-0 scores but hasn't started
600
- const event = {
601
- status: 'live',
602
- is_live: true,
603
- in_play: { period: '3' },
604
- scores: {
605
- home: { periods: { period_1: 33, period_2: 32, period_3: 12, period_4: 0 } },
606
- away: { periods: { period_1: 23, period_2: 27, period_3: 18, period_4: 0 } },
607
- },
608
- };
609
-
610
- const result = detectCompletedPeriods(event);
611
-
612
- expect(result).not.toBeNull();
613
- expect(result?.completedPeriods).toEqual([1, 2]); // Only periods 1 and 2 are complete
614
- expect(result?.currentPeriod).toBe(3);
615
- // Period 3 is currently being played, so NOT complete
616
- // Period 4 has data (0-0) but hasn't started, so NOT complete
617
- });
618
-
619
- it('Bug Fix: Should mark all periods as complete for completed game', () => {
620
- const event = {
621
- status: 'completed',
622
- is_live: false,
623
- scores: {
624
- home: { periods: { period_1: 33, period_2: 32, period_3: 25, period_4: 20 } },
625
- away: { periods: { period_1: 23, period_2: 27, period_3: 18, period_4: 22 } },
626
- },
627
- };
628
-
629
- const result = detectCompletedPeriods(event);
630
-
631
- expect(result).not.toBeNull();
632
- expect(result?.completedPeriods).toEqual([1, 2, 3, 4]);
633
- // All periods complete because game status is 'completed'
634
- });
635
-
636
- it('Bug Fix: Should NOT mark current period as complete during transition to period 4', () => {
637
- const event = {
638
- status: 'live',
639
- is_live: true,
640
- in_play: { period: '4' },
641
- scores: {
642
- home: { periods: { period_1: 33, period_2: 32, period_3: 25, period_4: 5 } },
643
- away: { periods: { period_1: 23, period_2: 27, period_3: 18, period_4: 3 } },
644
- },
645
- };
646
-
647
- const result = detectCompletedPeriods(event);
648
-
649
- expect(result).not.toBeNull();
650
- expect(result?.completedPeriods).toEqual([1, 2, 3]); // Periods 1, 2, 3 are complete
651
- expect(result?.currentPeriod).toBe(4);
652
- // Period 4 is currently in_play, so NOT complete yet
653
- });
654
-
655
- it('Basketball at halftime should mark periods 1 AND 2 as complete (quarters-based)', () => {
656
- const event = {
657
- sport: {
658
- id: 'basketball',
659
- name: 'Basketball',
660
- },
661
- fixture: {
662
- id: 'nba-halftime-123',
663
- status: 'half',
664
- is_live: true,
665
- },
666
- scores: {
667
- home: {
668
- total: 52.0,
669
- periods: {
670
- period_1: 25.0,
671
- period_2: 27.0,
672
- },
673
- },
674
- away: {
675
- total: 48.0,
676
- periods: {
677
- period_1: 22.0,
678
- period_2: 26.0,
679
- },
680
- },
681
- },
682
- in_play: {
683
- period: 'half',
684
- },
685
- };
686
-
687
- const result = detectCompletedPeriods(event);
688
-
689
- expect(result).not.toBeNull();
690
- expect(result?.completedPeriods).toEqual([1, 2]); // Both Q1 and Q2 complete at halftime
691
- expect(result?.readyForResolution).toBe(true);
692
- expect(result?.periodScores['period1']).toEqual({ home: 25.0, away: 22.0 });
693
- expect(result?.periodScores['period2']).toEqual({ home: 27.0, away: 26.0 });
694
- });
695
-
696
- it('Basketball at halftime with status "halftime" should mark periods 1 AND 2 as complete', () => {
697
- const event = {
698
- sport: {
699
- id: 'basketball',
700
- name: 'Basketball',
701
- },
702
- fixture: {
703
- id: 'nba-halftime-456',
704
- status: 'halftime',
705
- is_live: true,
706
- },
707
- scores: {
708
- home: {
709
- total: 55.0,
710
- periods: {
711
- period_1: 28.0,
712
- period_2: 27.0,
713
- },
714
- },
715
- away: {
716
- total: 50.0,
717
- periods: {
718
- period_1: 24.0,
719
- period_2: 26.0,
720
- },
721
- },
722
- },
723
- in_play: {
724
- period: '2',
725
- },
726
- };
727
-
728
- const result = detectCompletedPeriods(event);
729
-
730
- expect(result).not.toBeNull();
731
- expect(result?.completedPeriods).toEqual([1, 2]); // Both Q1 and Q2 complete at halftime
732
- expect(result?.readyForResolution).toBe(true);
733
- });
734
-
735
- it('Football at halftime should mark periods 1 AND 2 as complete (quarters-based)', () => {
736
- const event = {
737
- sport: {
738
- id: 'football',
739
- name: 'Football',
740
- },
741
- fixture: {
742
- id: 'nfl-halftime-789',
743
- status: 'half',
744
- is_live: true,
745
- },
746
- scores: {
747
- home: {
748
- total: 21.0,
749
- periods: {
750
- period_1: 7.0,
751
- period_2: 14.0,
752
- },
753
- },
754
- away: {
755
- total: 10.0,
756
- periods: {
757
- period_1: 3.0,
758
- period_2: 7.0,
759
- },
760
- },
761
- },
762
- in_play: {
763
- period: 'half',
764
- },
765
- };
766
-
767
- const result = detectCompletedPeriods(event);
768
-
769
- expect(result).not.toBeNull();
770
- expect(result?.completedPeriods).toEqual([1, 2]); // Both Q1 and Q2 complete at halftime
771
- expect(result?.readyForResolution).toBe(true);
772
- expect(result?.periodScores['period1']).toEqual({ home: 7.0, away: 3.0 });
773
- expect(result?.periodScores['period2']).toEqual({ home: 14.0, away: 7.0 });
774
- });
775
-
776
- it('Football at halftime with status "halftime" should mark periods 1 AND 2 as complete', () => {
777
- const event = {
778
- sport: {
779
- id: 'football',
780
- name: 'American Football',
781
- },
782
- fixture: {
783
- id: 'nfl-halftime-890',
784
- status: 'halftime',
785
- is_live: true,
786
- },
787
- scores: {
788
- home: {
789
- total: 17.0,
790
- periods: {
791
- period_1: 10.0,
792
- period_2: 7.0,
793
- },
794
- },
795
- away: {
796
- total: 14.0,
797
- periods: {
798
- period_1: 7.0,
799
- period_2: 7.0,
800
- },
801
- },
802
- },
803
- in_play: {
804
- period: '2',
805
- },
806
- };
807
-
808
- const result = detectCompletedPeriods(event);
809
-
810
- expect(result).not.toBeNull();
811
- expect(result?.completedPeriods).toEqual([1, 2]); // Both Q1 and Q2 complete at halftime
812
- expect(result?.readyForResolution).toBe(true);
813
- });
814
-
815
- it('Baseball at halftime should mark periods 1-5 as complete (innings-based)', () => {
816
- const event = {
817
- sport: {
818
- id: 'baseball',
819
- name: 'Baseball',
820
- },
821
- fixture: {
822
- id: 'mlb-halftime-789',
823
- status: 'half',
824
- is_live: true,
825
- },
826
- scores: {
827
- home: {
828
- total: 3.0,
829
- periods: {
830
- period_1: 0.0,
831
- period_2: 1.0,
832
- period_3: 1.0,
833
- period_4: 0.0,
834
- period_5: 1.0,
835
- },
836
- },
837
- away: {
838
- total: 2.0,
839
- periods: {
840
- period_1: 1.0,
841
- period_2: 0.0,
842
- period_3: 0.0,
843
- period_4: 1.0,
844
- period_5: 0.0,
845
- },
846
- },
847
- },
848
- in_play: {
849
- period: 'half',
850
- },
851
- };
852
-
853
- const result = detectCompletedPeriods(event);
854
-
855
- expect(result).not.toBeNull();
856
- expect(result?.completedPeriods).toEqual([1, 2, 3, 4, 5]); // First 5 innings complete at halftime
857
- expect(result?.readyForResolution).toBe(true);
858
- expect(result?.periodScores['period1']).toEqual({ home: 0.0, away: 1.0 });
859
- expect(result?.periodScores['period5']).toEqual({ home: 1.0, away: 0.0 });
860
- });
861
-
862
- it('Soccer at halftime should mark only period 1 as complete (halves-based)', () => {
863
- const event = {
864
- sport: {
865
- id: 'soccer',
866
- name: 'Soccer',
867
- },
868
- fixture: {
869
- id: 'soccer-halftime-101',
870
- status: 'halftime',
871
- is_live: true,
872
- },
873
- scores: {
874
- home: {
875
- total: 2.0,
876
- periods: {
877
- period_1: 2.0,
878
- },
879
- },
880
- away: {
881
- total: 1.0,
882
- periods: {
883
- period_1: 1.0,
884
- },
885
- },
886
- },
887
- in_play: {
888
- period: 'half',
889
- },
890
- };
891
-
892
- const result = detectCompletedPeriods(event);
893
-
894
- expect(result).not.toBeNull();
895
- expect(result?.completedPeriods).toEqual([1]); // Only first half complete at halftime
896
- expect(result?.readyForResolution).toBe(true);
897
- expect(result?.periodScores['period1']).toEqual({ home: 2.0, away: 1.0 });
898
- });
899
-
900
- it('Hockey at halftime should NOT mark any periods as complete (period-based, no halftime concept)', () => {
901
- const event = {
902
- sport: {
903
- id: 'hockey',
904
- name: 'Hockey',
905
- },
906
- fixture: {
907
- id: 'nhl-halftime-202',
908
- status: 'half',
909
- is_live: true,
910
- },
911
- scores: {
912
- home: {
913
- total: 2.0,
914
- periods: {
915
- period_1: 1.0,
916
- period_2: 1.0,
917
- },
918
- },
919
- away: {
920
- total: 1.0,
921
- periods: {
922
- period_1: 0.0,
923
- period_2: 1.0,
924
- },
925
- },
926
- },
927
- in_play: {
928
- period: 'half',
929
- },
930
- };
931
-
932
- const result = detectCompletedPeriods(event);
933
-
934
- // Hockey doesn't have traditional halftime, so halftime status shouldn't mark periods complete
935
- // Periods should only be marked complete based on in_play.period progression
936
- expect(result).toBeNull();
937
- });
938
-
939
- it('Unknown sport at halftime should default to PERIOD_BASED (no halftime processing)', () => {
940
- const event = {
941
- sport: {
942
- id: 'unknown_sport',
943
- name: 'Unknown Sport',
944
- },
945
- fixture: {
946
- id: 'unknown-halftime-123',
947
- status: 'half',
948
- is_live: true,
949
- },
950
- scores: {
951
- home: {
952
- total: 50.0,
953
- periods: {
954
- period_1: 25.0,
955
- period_2: 25.0,
956
- },
957
- },
958
- away: {
959
- total: 40.0,
960
- periods: {
961
- period_1: 20.0,
962
- period_2: 20.0,
963
- },
964
- },
965
- },
966
- in_play: {
967
- period: 'half',
968
- },
969
- };
970
-
971
- const result = detectCompletedPeriods(event);
972
-
973
- // Unknown sports default to PERIOD_BASED (no halftime processing)
974
- // So halftime status should NOT mark any periods as complete
975
- expect(result).toBeNull();
976
- });
977
-
978
- it('Event with missing sport.id should default to PERIOD_BASED (no halftime processing)', () => {
979
- const event = {
980
- sport: {
981
- name: 'Some Sport',
982
- },
983
- fixture: {
984
- id: 'no-sport-id-123',
985
- status: 'half',
986
- is_live: true,
987
- },
988
- scores: {
989
- home: {
990
- total: 50.0,
991
- periods: {
992
- period_1: 25.0,
993
- period_2: 25.0,
994
- },
995
- },
996
- away: {
997
- total: 40.0,
998
- periods: {
999
- period_1: 20.0,
1000
- period_2: 20.0,
1001
- },
1002
- },
1003
- },
1004
- in_play: {
1005
- period: 'half',
1006
- },
1007
- };
1008
-
1009
- const result = detectCompletedPeriods(event);
1010
-
1011
- // Missing sport.id defaults to PERIOD_BASED (no halftime processing)
1012
- expect(result).toBeNull();
1013
- });
1014
- });
1015
-
1016
- describe('canResolveMarketsForEvent', () => {
1017
- describe('Single typeId checks', () => {
1018
- it('Should return true for 1st period typeId when period 1 is complete (Soccer 2nd half)', () => {
1019
- const result = canResolveMarketsForEvent(MockSoccerLiveSecondHalf, 10021, SportPeriodType.HALVES_BASED);
1020
- expect(result).toBe(true);
1021
- });
1022
-
1023
- it('Should return false for 2nd period typeId when only period 1 is complete', () => {
1024
- const result = canResolveMarketsForEvent(MockSoccerLiveSecondHalf, 10022, SportPeriodType.HALVES_BASED);
1025
- expect(result).toBe(false);
1026
- });
1027
-
1028
- it('Should return false for full game typeId during live game', () => {
1029
- const result = canResolveMarketsForEvent(MockSoccerLiveSecondHalf, 10001, SportPeriodType.HALVES_BASED);
1030
- expect(result).toBe(false);
1031
- });
1032
-
1033
- it('Should return true for full game typeId when game is completed', () => {
1034
- const result = canResolveMarketsForEvent(MockSoccerCompletedEvent, 10001, SportPeriodType.HALVES_BASED);
1035
- expect(result).toBe(true);
1036
- });
1037
-
1038
- it('Should return true for 1st quarter typeId when quarter 1 complete (NFL)', () => {
1039
- const result = canResolveMarketsForEvent(
1040
- MockNFLLiveThirdQuarter,
1041
- 10021,
1042
- SportPeriodType.QUARTERS_BASED
1043
- );
1044
- expect(result).toBe(true);
1045
- });
1046
-
1047
- it('Should return true for 2nd quarter typeId when quarters 1-2 complete (NFL)', () => {
1048
- const result = canResolveMarketsForEvent(
1049
- MockNFLLiveThirdQuarter,
1050
- 10022,
1051
- SportPeriodType.QUARTERS_BASED
1052
- );
1053
- expect(result).toBe(true);
1054
- });
1055
-
1056
- it('Should return false for 3rd quarter typeId during 3rd quarter (NFL)', () => {
1057
- const result = canResolveMarketsForEvent(
1058
- MockNFLLiveThirdQuarter,
1059
- 10023,
1060
- SportPeriodType.QUARTERS_BASED
1061
- );
1062
- expect(result).toBe(false);
1063
- });
1064
-
1065
- it('Should return true for all quarter typeIds when game is completed (NFL)', () => {
1066
- expect(canResolveMarketsForEvent(MockNFLCompletedEvent, 10021, SportPeriodType.QUARTERS_BASED)).toBe(
1067
- true
1068
- );
1069
- expect(canResolveMarketsForEvent(MockNFLCompletedEvent, 10022, SportPeriodType.QUARTERS_BASED)).toBe(
1070
- true
1071
- );
1072
- expect(canResolveMarketsForEvent(MockNFLCompletedEvent, 10023, SportPeriodType.QUARTERS_BASED)).toBe(
1073
- true
1074
- );
1075
- expect(canResolveMarketsForEvent(MockNFLCompletedEvent, 10024, SportPeriodType.QUARTERS_BASED)).toBe(
1076
- true
1077
- );
1078
- });
1079
-
1080
- it('Should return false when no periods are complete', () => {
1081
- const result = canResolveMarketsForEvent(MockSoccerLiveFirstHalf, 10021, SportPeriodType.HALVES_BASED);
1082
- expect(result).toBe(false);
1083
- });
1084
-
1085
- it('Should return false for non-existent typeId', () => {
1086
- const result = canResolveMarketsForEvent(MockSoccerLiveSecondHalf, 99999, SportPeriodType.HALVES_BASED);
1087
- expect(result).toBe(false);
1088
- });
1089
- });
1090
-
1091
- describe('Batch typeIds checks', () => {
1092
- it('Should return only resolvable typeIds for live soccer in 2nd half', () => {
1093
- const typeIds = [10021, 10022, 10031, 10001];
1094
- const result = filterMarketsThatCanBeResolved(
1095
- MockSoccerLiveSecondHalf,
1096
- typeIds,
1097
- SportPeriodType.HALVES_BASED
1098
- );
1099
-
1100
- expect(result).toEqual([10021, 10031]); // Only period 1 typeIds
1101
- });
1102
-
1103
- it('Should exclude full game typeIds during live game', () => {
1104
- const typeIds = [10021, 10001, 10002, 10003];
1105
- const result = filterMarketsThatCanBeResolved(
1106
- MockNFLLiveThirdQuarter,
1107
- typeIds,
1108
- SportPeriodType.QUARTERS_BASED
1109
- );
1110
-
1111
- expect(result).toEqual([10021]); // Full game typeIds excluded
1112
- });
1113
-
1114
- it('Should include full game typeIds when game is completed', () => {
1115
- const typeIds = [10021, 10022, 10001, 10002];
1116
- const result = filterMarketsThatCanBeResolved(
1117
- MockSoccerCompletedEvent,
1118
- typeIds,
1119
- SportPeriodType.HALVES_BASED
1120
- );
1121
-
1122
- expect(result).toEqual([10021, 10022, 10001, 10002]);
1123
- });
1124
-
1125
- it('Should return empty array when no typeIds are resolvable', () => {
1126
- const typeIds = [10022, 10023, 10024];
1127
- const result = filterMarketsThatCanBeResolved(
1128
- MockSoccerLiveSecondHalf,
1129
- typeIds,
1130
- SportPeriodType.HALVES_BASED
1131
- );
1132
-
1133
- expect(result).toEqual([]);
1134
- });
1135
-
1136
- it('Should return multiple period typeIds for NFL game in 3rd quarter', () => {
1137
- const typeIds = [10021, 10022, 10023, 10024, 10031, 10032, 10051];
1138
- const result = filterMarketsThatCanBeResolved(
1139
- MockNFLLiveThirdQuarter,
1140
- typeIds,
1141
- SportPeriodType.QUARTERS_BASED
1142
- );
1143
-
1144
- // Periods 1 and 2 are complete (period 2 also completes 1st half = 10051)
1145
- expect(result).toEqual([10021, 10022, 10031, 10032, 10051]);
1146
- });
1147
-
1148
- it('Should handle all 9 periods for completed MLB game', () => {
1149
- const typeIds = [10021, 10022, 10023, 10024, 10025, 10026, 10027, 10028, 10029];
1150
- const result = filterMarketsThatCanBeResolved(
1151
- MockMLBCompletedEvent,
1152
- typeIds,
1153
- SportPeriodType.INNINGS_BASED
1154
- );
1155
-
1156
- expect(result).toEqual(typeIds); // All 9 innings complete
1157
- });
1158
-
1159
- it('Should return empty array when no periods complete', () => {
1160
- const typeIds = [10021, 10022, 10031];
1161
- const result = filterMarketsThatCanBeResolved(
1162
- MockSoccerLiveFirstHalf,
1163
- typeIds,
1164
- SportPeriodType.HALVES_BASED
1165
- );
1166
-
1167
- expect(result).toEqual([]);
1168
- });
1169
- });
1170
-
1171
- describe('Multiple typeIds boolean array checks', () => {
1172
- it('Should return boolean array for live soccer in 2nd half', () => {
1173
- const typeIds = [10021, 10022, 10031, 10001];
1174
- const result = canResolveMultipleTypeIdsForEvent(
1175
- MockSoccerLiveSecondHalf,
1176
- typeIds,
1177
- SportPeriodType.HALVES_BASED
1178
- );
1179
-
1180
- expect(result).toEqual([true, false, true, false]); // Period 1 typeIds are true, period 2 and full game are false
1181
- });
1182
-
1183
- it('Should return false for full game typeIds during live game', () => {
1184
- const typeIds = [10021, 10001, 10002, 10003];
1185
- const result = canResolveMultipleTypeIdsForEvent(
1186
- MockNFLLiveThirdQuarter,
1187
- typeIds,
1188
- SportPeriodType.QUARTERS_BASED
1189
- );
1190
-
1191
- expect(result).toEqual([true, false, false, false]); // Only period 1 is true
1192
- });
1193
-
1194
- it('Should return all true for completed game', () => {
1195
- const typeIds = [10021, 10022, 10001, 10002];
1196
- const result = canResolveMultipleTypeIdsForEvent(
1197
- MockSoccerCompletedEvent,
1198
- typeIds,
1199
- SportPeriodType.HALVES_BASED
1200
- );
1201
-
1202
- expect(result).toEqual([true, true, true, true]); // All complete
1203
- });
1204
-
1205
- it('Should return all false when no typeIds are resolvable', () => {
1206
- const typeIds = [10022, 10023, 10024];
1207
- const result = canResolveMultipleTypeIdsForEvent(
1208
- MockSoccerLiveSecondHalf,
1209
- typeIds,
1210
- SportPeriodType.HALVES_BASED
1211
- );
1212
-
1213
- expect(result).toEqual([false, false, false]);
1214
- });
1215
-
1216
- it('Should return mixed booleans for NFL game in 3rd quarter', () => {
1217
- const typeIds = [10021, 10022, 10023, 10024, 10031, 10032, 10051];
1218
- const result = canResolveMultipleTypeIdsForEvent(
1219
- MockNFLLiveThirdQuarter,
1220
- typeIds,
1221
- SportPeriodType.QUARTERS_BASED
1222
- );
1223
-
1224
- // Periods 1 and 2 are complete (period 2 also completes 1st half = 10051)
1225
- expect(result).toEqual([true, true, false, false, true, true, true]);
1226
- });
1227
-
1228
- it('Should handle all 9 periods for completed MLB game', () => {
1229
- const typeIds = [10021, 10022, 10023, 10024, 10025, 10026, 10027, 10028, 10029];
1230
- const result = canResolveMultipleTypeIdsForEvent(
1231
- MockMLBCompletedEvent,
1232
- typeIds,
1233
- SportPeriodType.INNINGS_BASED
1234
- );
1235
-
1236
- expect(result).toEqual([true, true, true, true, true, true, true, true, true]); // All 9 innings complete
1237
- });
1238
-
1239
- it('Should return all false when no periods complete', () => {
1240
- const typeIds = [10021, 10022, 10031];
1241
- const result = canResolveMultipleTypeIdsForEvent(
1242
- MockSoccerLiveFirstHalf,
1243
- typeIds,
1244
- SportPeriodType.HALVES_BASED
1245
- );
1246
-
1247
- expect(result).toEqual([false, false, false]);
1248
- });
1249
-
1250
- it('Should work with numeric sport type parameter', () => {
1251
- const typeIds = [10021, 10022];
1252
- const result = canResolveMultipleTypeIdsForEvent(MockSoccerLiveSecondHalf, typeIds, 0); // 0 = HALVES_BASED
1253
-
1254
- expect(result).toEqual([true, false]);
1255
- });
1256
- });
1257
-
1258
- describe('Edge cases', () => {
1259
- it('Should handle event with no completed periods', () => {
1260
- const result = canResolveMarketsForEvent(
1261
- MockSoccerLiveFirstHalfInProgress,
1262
- 10021,
1263
- SportPeriodType.HALVES_BASED
1264
- );
1265
- expect(result).toBe(false);
1266
- });
1267
-
1268
- it('Should respect full game typeIds list', () => {
1269
- // Test all full game typeIds
1270
- const fullGameTypeIds = [0, 10001, 10002, 10003, 10004, 10010, 10011, 10012];
1271
-
1272
- fullGameTypeIds.forEach((typeId) => {
1273
- const result = canResolveMarketsForEvent(
1274
- MockNFLLiveThirdQuarter,
1275
- typeId,
1276
- SportPeriodType.QUARTERS_BASED
1277
- );
1278
- expect(result).toBe(false);
1279
- });
1280
- });
1281
-
1282
- it('Should work with sport type parameter for single typeId', () => {
1283
- const result = canResolveMarketsForEvent(MockSoccerLiveSecondHalf, 10021, SportPeriodType.HALVES_BASED);
1284
- expect(result).toBe(true);
1285
- });
1286
-
1287
- it('Should work with sport type parameter for batch typeIds', () => {
1288
- const result = filterMarketsThatCanBeResolved(
1289
- MockSoccerLiveSecondHalf,
1290
- [10021, 10022],
1291
- SportPeriodType.HALVES_BASED
1292
- );
1293
- expect(result).toEqual([10021]);
1294
- });
1295
- });
1296
-
1297
- describe('Real overtime NFL game (Rams vs 49ers)', () => {
1298
- it('Should detect all 5 periods complete including overtime', () => {
1299
- const result = detectCompletedPeriods(MockNFLCompletedWithOvertime);
1300
-
1301
- expect(result).not.toBeNull();
1302
- expect(result?.completedPeriods).toEqual([1, 2, 3, 4, 5]);
1303
- expect(result?.readyForResolution).toBe(true);
1304
- expect(result?.periodScores['period5']).toEqual({ home: 0.0, away: 3.0 });
1305
- });
1306
-
1307
- it('Should resolve all quarter typeIds (10021-10024) for completed overtime game', () => {
1308
- expect(
1309
- canResolveMarketsForEvent(MockNFLCompletedWithOvertime, 10021, SportPeriodType.QUARTERS_BASED)
1310
- ).toBe(true);
1311
- expect(
1312
- canResolveMarketsForEvent(MockNFLCompletedWithOvertime, 10022, SportPeriodType.QUARTERS_BASED)
1313
- ).toBe(true);
1314
- expect(
1315
- canResolveMarketsForEvent(MockNFLCompletedWithOvertime, 10023, SportPeriodType.QUARTERS_BASED)
1316
- ).toBe(true);
1317
- expect(
1318
- canResolveMarketsForEvent(MockNFLCompletedWithOvertime, 10024, SportPeriodType.QUARTERS_BASED)
1319
- ).toBe(true);
1320
- });
1321
-
1322
- it('Should NOT resolve overtime period typeId (10025) - overtime has no specific markets', () => {
1323
- const result = canResolveMarketsForEvent(
1324
- MockNFLCompletedWithOvertime,
1325
- 10025,
1326
- SportPeriodType.QUARTERS_BASED
1327
- );
1328
- expect(result).toBe(false);
1329
- });
1330
-
1331
- it('Should resolve full game typeIds for completed overtime game', () => {
1332
- expect(
1333
- canResolveMarketsForEvent(MockNFLCompletedWithOvertime, 10001, SportPeriodType.QUARTERS_BASED)
1334
- ).toBe(true);
1335
- expect(
1336
- canResolveMarketsForEvent(MockNFLCompletedWithOvertime, 10002, SportPeriodType.QUARTERS_BASED)
1337
- ).toBe(true);
1338
- });
1339
-
1340
- it('Should return all resolvable typeIds excluding overtime in batch check', () => {
1341
- const typeIds = [10021, 10022, 10023, 10024, 10025, 10001];
1342
- const result = filterMarketsThatCanBeResolved(
1343
- MockNFLCompletedWithOvertime,
1344
- typeIds,
1345
- SportPeriodType.QUARTERS_BASED
1346
- );
1347
-
1348
- // 10025 (overtime) should not be included as period 5 has no specific markets
1349
- expect(result).toEqual([10021, 10022, 10023, 10024, 10001]);
1350
- });
1351
-
1352
- it('Should return false for 8th period typeId (period did not occur)', () => {
1353
- const result = canResolveMarketsForEvent(
1354
- MockNFLCompletedWithOvertime,
1355
- 10028,
1356
- SportPeriodType.QUARTERS_BASED
1357
- );
1358
- expect(result).toBe(false);
1359
- });
1360
-
1361
- it('Should return false for 9th period typeId (period did not occur)', () => {
1362
- const result = canResolveMarketsForEvent(
1363
- MockNFLCompletedWithOvertime,
1364
- 10029,
1365
- SportPeriodType.QUARTERS_BASED
1366
- );
1367
- expect(result).toBe(false);
1368
- });
1369
-
1370
- it('Should not include non-existent periods in batch check', () => {
1371
- const typeIds = [10021, 10022, 10023, 10024, 10025, 10028, 10029];
1372
- const result = filterMarketsThatCanBeResolved(
1373
- MockNFLCompletedWithOvertime,
1374
- typeIds,
1375
- SportPeriodType.QUARTERS_BASED
1376
- );
1377
-
1378
- // Periods 1-4 are resolvable; period 5 (overtime) has no markets; periods 8-9 did not occur
1379
- expect(result).toEqual([10021, 10022, 10023, 10024]);
1380
- });
1381
- });
1382
-
1383
- describe('Sport-type-specific resolution for typeId 10051 (1st half)', () => {
1384
- it('Soccer (HALVES_BASED): Should resolve typeId 10051 after period 1', () => {
1385
- // Soccer: Period 1 = 1st half
1386
- const result = canResolveMarketsForEvent(MockSoccerLiveSecondHalf, 10051, SportPeriodType.HALVES_BASED);
1387
- expect(result).toBe(true);
1388
- });
1389
-
1390
- it('NFL (QUARTERS_BASED): Should resolve typeId 10051 after period 2', () => {
1391
- // NFL: Period 2 completes 1st half (quarters 1+2)
1392
- const result = canResolveMarketsForEvent(
1393
- MockNFLLiveThirdQuarter,
1394
- 10051,
1395
- SportPeriodType.QUARTERS_BASED
1396
- );
1397
- expect(result).toBe(true);
1398
- });
1399
-
1400
- it('NFL (QUARTERS_BASED): Should NOT resolve typeId 10051 after only period 1', () => {
1401
- // Create mock with only period 1 complete
1402
- const mockNFLFirstQuarter = {
1403
- ...MockNFLLiveThirdQuarter,
1404
- scores: {
1405
- home: { total: 7.0, periods: { period_1: 7.0 } },
1406
- away: { total: 3.0, periods: { period_1: 3.0 } },
1407
- },
1408
- in_play: { period: '2', clock: '5:00' },
1409
- };
1410
-
1411
- const result = canResolveMarketsForEvent(mockNFLFirstQuarter, 10051, SportPeriodType.QUARTERS_BASED);
1412
- expect(result).toBe(false);
1413
- });
1414
-
1415
- it('MLB (INNINGS_BASED): Should resolve typeId 10051 after period 5', () => {
1416
- // MLB: Period 5 completes 1st half (innings 1-5)
1417
- const result = canResolveMarketsForEvent(MockMLBLiveSixthInning, 10051, SportPeriodType.INNINGS_BASED);
1418
- expect(result).toBe(true);
1419
- });
1420
-
1421
- it('MLB (INNINGS_BASED): Should NOT resolve typeId 10051 after only period 4', () => {
1422
- // Create mock with only period 1-4 complete
1423
- const mockMLBFourthInning = {
1424
- ...MockMLBLiveSixthInning,
1425
- scores: {
1426
- home: {
1427
- total: 3.0,
1428
- periods: {
1429
- period_1: 0.0,
1430
- period_2: 2.0,
1431
- period_3: 1.0,
1432
- period_4: 0.0,
1433
- },
1434
- },
1435
- away: {
1436
- total: 1.0,
1437
- periods: {
1438
- period_1: 1.0,
1439
- period_2: 0.0,
1440
- period_3: 0.0,
1441
- period_4: 0.0,
1442
- },
1443
- },
1444
- },
1445
- in_play: { period: '5', clock: null },
1446
- };
1447
-
1448
- const result = canResolveMarketsForEvent(mockMLBFourthInning, 10051, SportPeriodType.INNINGS_BASED);
1449
- expect(result).toBe(false);
1450
- });
1451
- });
1452
-
1453
- describe('Sport-type-specific resolution for typeId 10052 (2nd half)', () => {
1454
- it('Soccer (HALVES_BASED): Should resolve typeId 10052 after period 2', () => {
1455
- const result = canResolveMarketsForEvent(MockSoccerCompletedEvent, 10052, SportPeriodType.HALVES_BASED);
1456
- expect(result).toBe(true);
1457
- });
1458
-
1459
- it('NFL (QUARTERS_BASED): Should resolve typeId 10052 after period 4', () => {
1460
- const result = canResolveMarketsForEvent(MockNFLCompletedEvent, 10052, SportPeriodType.QUARTERS_BASED);
1461
- expect(result).toBe(true);
1462
- });
1463
-
1464
- it('MLB (INNINGS_BASED): Should resolve typeId 10052 after period 9', () => {
1465
- const result = canResolveMarketsForEvent(MockMLBCompletedEvent, 10052, SportPeriodType.INNINGS_BASED);
1466
- expect(result).toBe(true);
1467
- });
1468
- });
1469
-
1470
- describe('Error handling for invalid sport type numbers', () => {
1471
- it('Should throw error for invalid sport type number when using numeric parameter', () => {
1472
- const typeIds = [10021, 10022];
1473
-
1474
- // Invalid sport type number (must be 0-3)
1475
- expect(() => {
1476
- canResolveMultipleTypeIdsForEvent(MockSoccerLiveSecondHalf, typeIds, 99);
1477
- }).toThrow('Invalid sport type number: 99. Must be 0 (halves), 1 (quarters), 2 (innings), or 3 (period).');
1478
- });
1479
-
1480
- it('Should throw error for negative sport type number', () => {
1481
- const typeIds = [10021];
1482
-
1483
- expect(() => {
1484
- canResolveMultipleTypeIdsForEvent(MockSoccerLiveSecondHalf, typeIds, -1);
1485
- }).toThrow('Invalid sport type number: -1. Must be 0 (halves), 1 (quarters), 2 (innings), or 3 (period).');
1486
- });
1487
- });
1488
- });
1489
- });
1
+ import {
2
+ detectCompletedPeriods,
3
+ canResolveMarketsForEvent,
4
+ canResolveMultipleTypeIdsForEvent,
5
+ filterMarketsThatCanBeResolved,
6
+ } from '../../utils/resolution';
7
+ import { SportPeriodType } from '../../types/resolution';
8
+ import {
9
+ MockSoccerCompletedEvent,
10
+ MockSoccerLiveSecondHalf,
11
+ MockSoccerLiveFirstHalf,
12
+ MockNFLCompletedEvent,
13
+ MockNFLLiveThirdQuarter,
14
+ MockMLBCompletedEvent,
15
+ MockMLBLiveSixthInning,
16
+ MockSoccerLiveFirstHalfInProgress,
17
+ MockNFLCompletedWithOvertime,
18
+ MockNBACompletedEvent,
19
+ MockNBALiveAtHalftime,
20
+ } from '../mock/MockOpticOddsEvents';
21
+
22
+ describe('Resolution Utils', () => {
23
+ describe('detectCompletedPeriods', () => {
24
+ it('Should detect completed periods for real completed soccer game (UEFA Europa League)', () => {
25
+ const result = detectCompletedPeriods(MockSoccerCompletedEvent);
26
+
27
+ expect(result).not.toBeNull();
28
+ expect(result?.completedPeriods).toEqual([1, 2]);
29
+ expect(result?.readyForResolution).toBe(true);
30
+ expect(result?.periodScores['period1']).toEqual({ home: 1.0, away: 0.0 });
31
+ expect(result?.periodScores['period2']).toEqual({ home: 1.0, away: 0.0 });
32
+ });
33
+
34
+ it('Should detect completed first half in real live soccer game (2nd half)', () => {
35
+ const result = detectCompletedPeriods(MockSoccerLiveSecondHalf);
36
+
37
+ expect(result).not.toBeNull();
38
+ expect(result?.completedPeriods).toEqual([1]);
39
+ expect(result?.readyForResolution).toBe(true);
40
+ expect(result?.periodScores['period1']).toEqual({ home: 1.0, away: 1.0 });
41
+ expect(result?.currentPeriod).toBe(2);
42
+ });
43
+
44
+ it('Should return null for real live soccer game in first half (no completed periods)', () => {
45
+ const result = detectCompletedPeriods(MockSoccerLiveFirstHalf);
46
+
47
+ expect(result).toBeNull();
48
+ });
49
+
50
+ it('Should detect completed periods for real completed NFL game (Patriots vs Panthers)', () => {
51
+ const result = detectCompletedPeriods(MockNFLCompletedEvent);
52
+
53
+ expect(result).not.toBeNull();
54
+ expect(result?.completedPeriods).toEqual([1, 2, 3, 4]);
55
+ expect(result?.readyForResolution).toBe(true);
56
+ expect(result?.periodScores['period1']).toEqual({ home: 7.0, away: 6.0 });
57
+ expect(result?.periodScores['period2']).toEqual({ home: 21.0, away: 0.0 });
58
+ expect(result?.periodScores['period3']).toEqual({ home: 7.0, away: 0.0 });
59
+ expect(result?.periodScores['period4']).toEqual({ home: 7.0, away: 7.0 });
60
+ });
61
+
62
+ it('Should detect completed quarters in real live NFL game (3rd quarter)', () => {
63
+ const result = detectCompletedPeriods(MockNFLLiveThirdQuarter);
64
+
65
+ expect(result).not.toBeNull();
66
+ expect(result?.completedPeriods).toEqual([1, 2]);
67
+ expect(result?.readyForResolution).toBe(true);
68
+ expect(result?.periodScores['period1']).toEqual({ home: 7.0, away: 3.0 });
69
+ expect(result?.periodScores['period2']).toEqual({ home: 14.0, away: 7.0 });
70
+ expect(result?.currentPeriod).toBe(3);
71
+ });
72
+
73
+ it('Should detect completed innings for real completed MLB game (Tigers vs Guardians)', () => {
74
+ const result = detectCompletedPeriods(MockMLBCompletedEvent);
75
+
76
+ expect(result).not.toBeNull();
77
+ expect(result?.completedPeriods).toEqual([1, 2, 3, 4, 5, 6, 7, 8, 9]);
78
+ expect(result?.readyForResolution).toBe(true);
79
+ expect(result?.periodScores['period1']).toEqual({ home: 0.0, away: 0.0 });
80
+ expect(result?.periodScores['period3']).toEqual({ home: 0.0, away: 1.0 });
81
+ expect(result?.periodScores['period7']).toEqual({ home: 0.0, away: 4.0 });
82
+ expect(result?.periodScores['period8']).toEqual({ home: 2.0, away: 0.0 });
83
+ expect(result?.periodScores['period9']).toEqual({ home: 0.0, away: 0.0 });
84
+ });
85
+
86
+ it('Should detect completed innings in real live MLB game (6th inning)', () => {
87
+ const result = detectCompletedPeriods(MockMLBLiveSixthInning);
88
+
89
+ expect(result).not.toBeNull();
90
+ expect(result?.completedPeriods).toEqual([1, 2, 3, 4, 5]);
91
+ expect(result?.readyForResolution).toBe(true);
92
+ expect(result?.periodScores['period1']).toEqual({ home: 0.0, away: 1.0 });
93
+ expect(result?.periodScores['period2']).toEqual({ home: 2.0, away: 0.0 });
94
+ expect(result?.periodScores['period3']).toEqual({ home: 1.0, away: 0.0 });
95
+ expect(result?.periodScores['period4']).toEqual({ home: 0.0, away: 0.0 });
96
+ expect(result?.periodScores['period5']).toEqual({ home: 1.0, away: 1.0 });
97
+ expect(result?.currentPeriod).toBe(6);
98
+ });
99
+
100
+ it('Should detect completed periods for real completed NBA game (Warriors vs Suns)', () => {
101
+ const result = detectCompletedPeriods(MockNBACompletedEvent);
102
+
103
+ expect(result).not.toBeNull();
104
+ expect(result?.completedPeriods).toEqual([1, 2, 3, 4]);
105
+ expect(result?.readyForResolution).toBe(true);
106
+ expect(result?.periodScores['period1']).toEqual({ home: 33.0, away: 19.0 });
107
+ expect(result?.periodScores['period2']).toEqual({ home: 35.0, away: 30.0 });
108
+ expect(result?.periodScores['period3']).toEqual({ home: 24.0, away: 34.0 });
109
+ expect(result?.periodScores['period4']).toEqual({ home: 26.0, away: 24.0 });
110
+ });
111
+
112
+ it('Should detect completed quarters at halftime for real NBA game (Warriors vs Suns)', () => {
113
+ const result = detectCompletedPeriods(MockNBALiveAtHalftime);
114
+
115
+ expect(result).not.toBeNull();
116
+ expect(result?.completedPeriods).toEqual([1, 2]); // Both quarters complete at halftime
117
+ expect(result?.readyForResolution).toBe(true);
118
+ expect(result?.periodScores['period1']).toEqual({ home: 33.0, away: 19.0 });
119
+ expect(result?.periodScores['period2']).toEqual({ home: 35.0, away: 30.0 });
120
+ expect(result?.currentPeriod).toBe(2); // Highest period with data at halftime
121
+ });
122
+
123
+ it('Should return null for real live soccer game with non-numeric period indicator (1H)', () => {
124
+ const result = detectCompletedPeriods(MockSoccerLiveFirstHalfInProgress);
125
+
126
+ // Period 1 exists in data but period is "1H" (non-numeric) meaning first half is still in progress
127
+ // Period 1 is NOT complete until we see period_2 in the data or status becomes completed
128
+ expect(result).toBeNull();
129
+ });
130
+
131
+ it('Should detect completed periods for completed American Football game', () => {
132
+ const event = {
133
+ sport: {
134
+ id: 'football',
135
+ name: 'Football',
136
+ },
137
+ fixture: {
138
+ id: '20250930BEED03AA',
139
+ status: 'completed',
140
+ is_live: false,
141
+ },
142
+ scores: {
143
+ home: {
144
+ total: 28.0,
145
+ periods: {
146
+ period_1: 7.0,
147
+ period_2: 14.0,
148
+ period_3: 0.0,
149
+ period_4: 7.0,
150
+ },
151
+ },
152
+ away: {
153
+ total: 3.0,
154
+ periods: {
155
+ period_1: 3.0,
156
+ period_2: 0.0,
157
+ period_3: 0.0,
158
+ period_4: 0.0,
159
+ },
160
+ },
161
+ },
162
+ in_play: {
163
+ period: '4',
164
+ },
165
+ };
166
+
167
+ const result = detectCompletedPeriods(event);
168
+
169
+ expect(result).not.toBeNull();
170
+ expect(result?.completedPeriods).toEqual([1, 2, 3, 4]);
171
+ expect(result?.readyForResolution).toBe(true);
172
+ expect(result?.periodScores['period1']).toEqual({ home: 7.0, away: 3.0 });
173
+ expect(result?.periodScores['period2']).toEqual({ home: 14.0, away: 0.0 });
174
+ expect(result?.periodScores['period3']).toEqual({ home: 0.0, away: 0.0 });
175
+ expect(result?.periodScores['period4']).toEqual({ home: 7.0, away: 0.0 });
176
+ });
177
+
178
+ it('Should detect completed quarters in live American Football game', () => {
179
+ const event = {
180
+ sport: {
181
+ name: 'Football',
182
+ },
183
+ fixture: {
184
+ id: 'nfl-live-123',
185
+ status: 'live',
186
+ is_live: true,
187
+ },
188
+ scores: {
189
+ home: {
190
+ total: 21.0,
191
+ periods: {
192
+ period_1: 7.0,
193
+ period_2: 14.0,
194
+ period_3: 0.0,
195
+ },
196
+ },
197
+ away: {
198
+ total: 10.0,
199
+ periods: {
200
+ period_1: 3.0,
201
+ period_2: 7.0,
202
+ period_3: 0.0,
203
+ },
204
+ },
205
+ },
206
+ in_play: {
207
+ period: '3',
208
+ },
209
+ };
210
+
211
+ const result = detectCompletedPeriods(event);
212
+
213
+ expect(result).not.toBeNull();
214
+ expect(result?.completedPeriods).toEqual([1, 2]);
215
+ expect(result?.readyForResolution).toBe(true);
216
+ expect(result?.periodScores['period1']).toEqual({ home: 7.0, away: 3.0 });
217
+ expect(result?.periodScores['period2']).toEqual({ home: 14.0, away: 7.0 });
218
+ expect(result?.currentPeriod).toBe(3);
219
+ });
220
+
221
+ it('Should detect all quarters complete in American Football overtime', () => {
222
+ const event = {
223
+ sport: {
224
+ name: 'American Football',
225
+ },
226
+ fixture: {
227
+ id: 'nfl-ot-456',
228
+ status: 'live',
229
+ is_live: true,
230
+ },
231
+ scores: {
232
+ home: {
233
+ total: 24.0,
234
+ periods: {
235
+ period_1: 7.0,
236
+ period_2: 7.0,
237
+ period_3: 3.0,
238
+ period_4: 7.0,
239
+ },
240
+ },
241
+ away: {
242
+ total: 24.0,
243
+ periods: {
244
+ period_1: 10.0,
245
+ period_2: 7.0,
246
+ period_3: 0.0,
247
+ period_4: 7.0,
248
+ },
249
+ },
250
+ },
251
+ in_play: {
252
+ period: 'overtime',
253
+ },
254
+ };
255
+
256
+ const result = detectCompletedPeriods(event);
257
+
258
+ expect(result).not.toBeNull();
259
+ expect(result?.completedPeriods).toEqual([1, 2, 3, 4]);
260
+ expect(result?.readyForResolution).toBe(true);
261
+ });
262
+
263
+ it('Should detect completed first half in live soccer game', () => {
264
+ const event = {
265
+ sport: {
266
+ name: 'Soccer',
267
+ },
268
+ fixture: {
269
+ id: 'soccer-123',
270
+ status: 'live',
271
+ is_live: true,
272
+ },
273
+ scores: {
274
+ home: {
275
+ total: 2.0,
276
+ periods: {
277
+ period_1: 1.0,
278
+ },
279
+ },
280
+ away: {
281
+ total: 1.0,
282
+ periods: {
283
+ period_1: 0.0,
284
+ },
285
+ },
286
+ },
287
+ in_play: {
288
+ period: '2',
289
+ },
290
+ };
291
+
292
+ const result = detectCompletedPeriods(event);
293
+
294
+ expect(result).not.toBeNull();
295
+ expect(result?.completedPeriods).toEqual([1]);
296
+ expect(result?.readyForResolution).toBe(true);
297
+ expect(result?.periodScores['period1']).toEqual({ home: 1.0, away: 0.0 });
298
+ });
299
+
300
+ it('Should detect completed quarters in live basketball game', () => {
301
+ const event = {
302
+ sport: {
303
+ name: 'Basketball',
304
+ },
305
+ fixture: {
306
+ id: 'nba-789',
307
+ status: 'live',
308
+ is_live: true,
309
+ },
310
+ scores: {
311
+ home: {
312
+ total: 65.0,
313
+ periods: {
314
+ period_1: 25.0,
315
+ period_2: 20.0,
316
+ },
317
+ },
318
+ away: {
319
+ total: 62.0,
320
+ periods: {
321
+ period_1: 22.0,
322
+ period_2: 18.0,
323
+ },
324
+ },
325
+ },
326
+ in_play: {
327
+ period: '3',
328
+ },
329
+ };
330
+
331
+ const result = detectCompletedPeriods(event);
332
+
333
+ expect(result).not.toBeNull();
334
+ expect(result?.completedPeriods).toEqual([1, 2]);
335
+ expect(result?.readyForResolution).toBe(true);
336
+ expect(result?.periodScores['period1']).toEqual({ home: 25.0, away: 22.0 });
337
+ expect(result?.periodScores['period2']).toEqual({ home: 20.0, away: 18.0 });
338
+ });
339
+
340
+ it('Should detect completed sets in live tennis match', () => {
341
+ const event = {
342
+ sport: {
343
+ name: 'Tennis',
344
+ },
345
+ fixture: {
346
+ id: 'tennis-101',
347
+ status: 'live',
348
+ is_live: true,
349
+ },
350
+ scores: {
351
+ home: {
352
+ total: 1.0,
353
+ periods: {
354
+ period_1: 6.0,
355
+ period_2: 3.0,
356
+ },
357
+ },
358
+ away: {
359
+ total: 1.0,
360
+ periods: {
361
+ period_1: 4.0,
362
+ period_2: 6.0,
363
+ },
364
+ },
365
+ },
366
+ in_play: {
367
+ period: '3',
368
+ },
369
+ };
370
+
371
+ const result = detectCompletedPeriods(event);
372
+
373
+ expect(result).not.toBeNull();
374
+ expect(result?.completedPeriods).toEqual([1, 2]);
375
+ expect(result?.readyForResolution).toBe(true);
376
+ expect(result?.periodScores['period1']).toEqual({ home: 6.0, away: 4.0 });
377
+ expect(result?.periodScores['period2']).toEqual({ home: 3.0, away: 6.0 });
378
+ });
379
+
380
+ it('Should detect completed periods in live hockey game', () => {
381
+ const event = {
382
+ sport: {
383
+ name: 'Ice Hockey',
384
+ },
385
+ fixture: {
386
+ id: 'nhl-202',
387
+ status: 'live',
388
+ is_live: true,
389
+ },
390
+ scores: {
391
+ home: {
392
+ total: 2.0,
393
+ periods: {
394
+ period_1: 1.0,
395
+ },
396
+ },
397
+ away: {
398
+ total: 1.0,
399
+ periods: {
400
+ period_1: 1.0,
401
+ },
402
+ },
403
+ },
404
+ in_play: {
405
+ period: '2',
406
+ },
407
+ };
408
+
409
+ const result = detectCompletedPeriods(event);
410
+
411
+ expect(result).not.toBeNull();
412
+ expect(result?.completedPeriods).toEqual([1]);
413
+ expect(result?.readyForResolution).toBe(true);
414
+ expect(result?.periodScores['period1']).toEqual({ home: 1.0, away: 1.0 });
415
+ });
416
+
417
+ it('Should detect all periods complete in hockey overtime', () => {
418
+ const event = {
419
+ sport: {
420
+ name: 'Hockey',
421
+ },
422
+ fixture: {
423
+ id: 'nhl-303',
424
+ status: 'live',
425
+ is_live: true,
426
+ },
427
+ scores: {
428
+ home: {
429
+ total: 3.0,
430
+ periods: {
431
+ period_1: 1.0,
432
+ period_2: 1.0,
433
+ period_3: 1.0,
434
+ },
435
+ },
436
+ away: {
437
+ total: 3.0,
438
+ periods: {
439
+ period_1: 0.0,
440
+ period_2: 2.0,
441
+ period_3: 1.0,
442
+ },
443
+ },
444
+ },
445
+ in_play: {
446
+ period: 'overtime',
447
+ },
448
+ };
449
+
450
+ const result = detectCompletedPeriods(event);
451
+
452
+ expect(result).not.toBeNull();
453
+ expect(result?.completedPeriods).toEqual([1, 2, 3]);
454
+ expect(result?.readyForResolution).toBe(true);
455
+ });
456
+
457
+ it('Should detect completed innings in live baseball game', () => {
458
+ const event = {
459
+ sport: {
460
+ name: 'Baseball',
461
+ },
462
+ fixture: {
463
+ id: 'mlb-404',
464
+ status: 'live',
465
+ is_live: true,
466
+ },
467
+ scores: {
468
+ home: {
469
+ total: 4.0,
470
+ periods: {
471
+ period_1: 1.0,
472
+ period_2: 0.0,
473
+ period_3: 2.0,
474
+ period_4: 1.0,
475
+ },
476
+ },
477
+ away: {
478
+ total: 3.0,
479
+ periods: {
480
+ period_1: 0.0,
481
+ period_2: 1.0,
482
+ period_3: 1.0,
483
+ period_4: 1.0,
484
+ },
485
+ },
486
+ },
487
+ in_play: {
488
+ period: '5',
489
+ },
490
+ };
491
+
492
+ const result = detectCompletedPeriods(event);
493
+
494
+ expect(result).not.toBeNull();
495
+ expect(result?.completedPeriods).toEqual([1, 2, 3, 4]);
496
+ expect(result?.readyForResolution).toBe(true);
497
+ });
498
+
499
+ it('Should detect completed sets in live volleyball match', () => {
500
+ const event = {
501
+ sport: {
502
+ name: 'Volleyball',
503
+ },
504
+ fixture: {
505
+ id: 'volleyball-505',
506
+ status: 'live',
507
+ is_live: true,
508
+ },
509
+ scores: {
510
+ home: {
511
+ total: 1.0,
512
+ periods: {
513
+ period_1: 25.0,
514
+ period_2: 22.0,
515
+ },
516
+ },
517
+ away: {
518
+ total: 1.0,
519
+ periods: {
520
+ period_1: 23.0,
521
+ period_2: 25.0,
522
+ },
523
+ },
524
+ },
525
+ in_play: {
526
+ period: '3',
527
+ },
528
+ };
529
+
530
+ const result = detectCompletedPeriods(event);
531
+
532
+ expect(result).not.toBeNull();
533
+ expect(result?.completedPeriods).toEqual([1, 2]);
534
+ expect(result?.readyForResolution).toBe(true);
535
+ });
536
+
537
+ it('Should return null for game not started', () => {
538
+ const event = {
539
+ sport: {
540
+ name: 'Soccer',
541
+ },
542
+ fixture: {
543
+ id: 'future-game',
544
+ status: 'scheduled',
545
+ is_live: false,
546
+ },
547
+ scores: {
548
+ home: {
549
+ total: 0.0,
550
+ periods: {},
551
+ },
552
+ away: {
553
+ total: 0.0,
554
+ periods: {},
555
+ },
556
+ },
557
+ in_play: {
558
+ period: null,
559
+ },
560
+ };
561
+
562
+ const result = detectCompletedPeriods(event);
563
+
564
+ expect(result).toBeNull();
565
+ });
566
+
567
+ it('Should return null for game in first period with no completed periods', () => {
568
+ const event = {
569
+ sport: {
570
+ name: 'Basketball',
571
+ },
572
+ fixture: {
573
+ id: 'early-game',
574
+ status: 'live',
575
+ is_live: true,
576
+ },
577
+ scores: {
578
+ home: {
579
+ total: 12.0,
580
+ periods: {},
581
+ },
582
+ away: {
583
+ total: 10.0,
584
+ periods: {},
585
+ },
586
+ },
587
+ in_play: {
588
+ period: '1',
589
+ },
590
+ };
591
+
592
+ const result = detectCompletedPeriods(event);
593
+
594
+ expect(result).toBeNull();
595
+ });
596
+
597
+ it('Bug Fix: Should NOT mark current period as complete when future periods have data (live in period 3)', () => {
598
+ // This tests the bug where OpticOdds API includes future period data with scores (including zeros)
599
+ // Period 3 is currently being played, period 4 has 0-0 scores but hasn't started
600
+ const event = {
601
+ status: 'live',
602
+ is_live: true,
603
+ in_play: { period: '3' },
604
+ scores: {
605
+ home: { periods: { period_1: 33, period_2: 32, period_3: 12, period_4: 0 } },
606
+ away: { periods: { period_1: 23, period_2: 27, period_3: 18, period_4: 0 } },
607
+ },
608
+ };
609
+
610
+ const result = detectCompletedPeriods(event);
611
+
612
+ expect(result).not.toBeNull();
613
+ expect(result?.completedPeriods).toEqual([1, 2]); // Only periods 1 and 2 are complete
614
+ expect(result?.currentPeriod).toBe(3);
615
+ // Period 3 is currently being played, so NOT complete
616
+ // Period 4 has data (0-0) but hasn't started, so NOT complete
617
+ });
618
+
619
+ it('Bug Fix: Should mark all periods as complete for completed game', () => {
620
+ const event = {
621
+ status: 'completed',
622
+ is_live: false,
623
+ scores: {
624
+ home: { periods: { period_1: 33, period_2: 32, period_3: 25, period_4: 20 } },
625
+ away: { periods: { period_1: 23, period_2: 27, period_3: 18, period_4: 22 } },
626
+ },
627
+ };
628
+
629
+ const result = detectCompletedPeriods(event);
630
+
631
+ expect(result).not.toBeNull();
632
+ expect(result?.completedPeriods).toEqual([1, 2, 3, 4]);
633
+ // All periods complete because game status is 'completed'
634
+ });
635
+
636
+ it('Bug Fix: Should NOT mark current period as complete during transition to period 4', () => {
637
+ const event = {
638
+ status: 'live',
639
+ is_live: true,
640
+ in_play: { period: '4' },
641
+ scores: {
642
+ home: { periods: { period_1: 33, period_2: 32, period_3: 25, period_4: 5 } },
643
+ away: { periods: { period_1: 23, period_2: 27, period_3: 18, period_4: 3 } },
644
+ },
645
+ };
646
+
647
+ const result = detectCompletedPeriods(event);
648
+
649
+ expect(result).not.toBeNull();
650
+ expect(result?.completedPeriods).toEqual([1, 2, 3]); // Periods 1, 2, 3 are complete
651
+ expect(result?.currentPeriod).toBe(4);
652
+ // Period 4 is currently in_play, so NOT complete yet
653
+ });
654
+
655
+ it('Basketball at halftime should mark periods 1 AND 2 as complete (quarters-based)', () => {
656
+ const event = {
657
+ sport: {
658
+ id: 'basketball',
659
+ name: 'Basketball',
660
+ },
661
+ fixture: {
662
+ id: 'nba-halftime-123',
663
+ status: 'half',
664
+ is_live: true,
665
+ },
666
+ scores: {
667
+ home: {
668
+ total: 52.0,
669
+ periods: {
670
+ period_1: 25.0,
671
+ period_2: 27.0,
672
+ },
673
+ },
674
+ away: {
675
+ total: 48.0,
676
+ periods: {
677
+ period_1: 22.0,
678
+ period_2: 26.0,
679
+ },
680
+ },
681
+ },
682
+ in_play: {
683
+ period: 'half',
684
+ },
685
+ };
686
+
687
+ const result = detectCompletedPeriods(event);
688
+
689
+ expect(result).not.toBeNull();
690
+ expect(result?.completedPeriods).toEqual([1, 2]); // Both Q1 and Q2 complete at halftime
691
+ expect(result?.readyForResolution).toBe(true);
692
+ expect(result?.periodScores['period1']).toEqual({ home: 25.0, away: 22.0 });
693
+ expect(result?.periodScores['period2']).toEqual({ home: 27.0, away: 26.0 });
694
+ });
695
+
696
+ it('Basketball at halftime with status "halftime" should mark periods 1 AND 2 as complete', () => {
697
+ const event = {
698
+ sport: {
699
+ id: 'basketball',
700
+ name: 'Basketball',
701
+ },
702
+ fixture: {
703
+ id: 'nba-halftime-456',
704
+ status: 'halftime',
705
+ is_live: true,
706
+ },
707
+ scores: {
708
+ home: {
709
+ total: 55.0,
710
+ periods: {
711
+ period_1: 28.0,
712
+ period_2: 27.0,
713
+ },
714
+ },
715
+ away: {
716
+ total: 50.0,
717
+ periods: {
718
+ period_1: 24.0,
719
+ period_2: 26.0,
720
+ },
721
+ },
722
+ },
723
+ in_play: {
724
+ period: '2',
725
+ },
726
+ };
727
+
728
+ const result = detectCompletedPeriods(event);
729
+
730
+ expect(result).not.toBeNull();
731
+ expect(result?.completedPeriods).toEqual([1, 2]); // Both Q1 and Q2 complete at halftime
732
+ expect(result?.readyForResolution).toBe(true);
733
+ });
734
+
735
+ it('Football at halftime should mark periods 1 AND 2 as complete (quarters-based)', () => {
736
+ const event = {
737
+ sport: {
738
+ id: 'football',
739
+ name: 'Football',
740
+ },
741
+ fixture: {
742
+ id: 'nfl-halftime-789',
743
+ status: 'half',
744
+ is_live: true,
745
+ },
746
+ scores: {
747
+ home: {
748
+ total: 21.0,
749
+ periods: {
750
+ period_1: 7.0,
751
+ period_2: 14.0,
752
+ },
753
+ },
754
+ away: {
755
+ total: 10.0,
756
+ periods: {
757
+ period_1: 3.0,
758
+ period_2: 7.0,
759
+ },
760
+ },
761
+ },
762
+ in_play: {
763
+ period: 'half',
764
+ },
765
+ };
766
+
767
+ const result = detectCompletedPeriods(event);
768
+
769
+ expect(result).not.toBeNull();
770
+ expect(result?.completedPeriods).toEqual([1, 2]); // Both Q1 and Q2 complete at halftime
771
+ expect(result?.readyForResolution).toBe(true);
772
+ expect(result?.periodScores['period1']).toEqual({ home: 7.0, away: 3.0 });
773
+ expect(result?.periodScores['period2']).toEqual({ home: 14.0, away: 7.0 });
774
+ });
775
+
776
+ it('Football at halftime with status "halftime" should mark periods 1 AND 2 as complete', () => {
777
+ const event = {
778
+ sport: {
779
+ id: 'football',
780
+ name: 'American Football',
781
+ },
782
+ fixture: {
783
+ id: 'nfl-halftime-890',
784
+ status: 'halftime',
785
+ is_live: true,
786
+ },
787
+ scores: {
788
+ home: {
789
+ total: 17.0,
790
+ periods: {
791
+ period_1: 10.0,
792
+ period_2: 7.0,
793
+ },
794
+ },
795
+ away: {
796
+ total: 14.0,
797
+ periods: {
798
+ period_1: 7.0,
799
+ period_2: 7.0,
800
+ },
801
+ },
802
+ },
803
+ in_play: {
804
+ period: '2',
805
+ },
806
+ };
807
+
808
+ const result = detectCompletedPeriods(event);
809
+
810
+ expect(result).not.toBeNull();
811
+ expect(result?.completedPeriods).toEqual([1, 2]); // Both Q1 and Q2 complete at halftime
812
+ expect(result?.readyForResolution).toBe(true);
813
+ });
814
+
815
+ it('Baseball at halftime should mark periods 1-5 as complete (innings-based)', () => {
816
+ const event = {
817
+ sport: {
818
+ id: 'baseball',
819
+ name: 'Baseball',
820
+ },
821
+ fixture: {
822
+ id: 'mlb-halftime-789',
823
+ status: 'half',
824
+ is_live: true,
825
+ },
826
+ scores: {
827
+ home: {
828
+ total: 3.0,
829
+ periods: {
830
+ period_1: 0.0,
831
+ period_2: 1.0,
832
+ period_3: 1.0,
833
+ period_4: 0.0,
834
+ period_5: 1.0,
835
+ },
836
+ },
837
+ away: {
838
+ total: 2.0,
839
+ periods: {
840
+ period_1: 1.0,
841
+ period_2: 0.0,
842
+ period_3: 0.0,
843
+ period_4: 1.0,
844
+ period_5: 0.0,
845
+ },
846
+ },
847
+ },
848
+ in_play: {
849
+ period: 'half',
850
+ },
851
+ };
852
+
853
+ const result = detectCompletedPeriods(event);
854
+
855
+ expect(result).not.toBeNull();
856
+ expect(result?.completedPeriods).toEqual([1, 2, 3, 4, 5]); // First 5 innings complete at halftime
857
+ expect(result?.readyForResolution).toBe(true);
858
+ expect(result?.periodScores['period1']).toEqual({ home: 0.0, away: 1.0 });
859
+ expect(result?.periodScores['period5']).toEqual({ home: 1.0, away: 0.0 });
860
+ });
861
+
862
+ it('Soccer at halftime should mark only period 1 as complete (halves-based)', () => {
863
+ const event = {
864
+ sport: {
865
+ id: 'soccer',
866
+ name: 'Soccer',
867
+ },
868
+ fixture: {
869
+ id: 'soccer-halftime-101',
870
+ status: 'halftime',
871
+ is_live: true,
872
+ },
873
+ scores: {
874
+ home: {
875
+ total: 2.0,
876
+ periods: {
877
+ period_1: 2.0,
878
+ },
879
+ },
880
+ away: {
881
+ total: 1.0,
882
+ periods: {
883
+ period_1: 1.0,
884
+ },
885
+ },
886
+ },
887
+ in_play: {
888
+ period: 'half',
889
+ },
890
+ };
891
+
892
+ const result = detectCompletedPeriods(event);
893
+
894
+ expect(result).not.toBeNull();
895
+ expect(result?.completedPeriods).toEqual([1]); // Only first half complete at halftime
896
+ expect(result?.readyForResolution).toBe(true);
897
+ expect(result?.periodScores['period1']).toEqual({ home: 2.0, away: 1.0 });
898
+ });
899
+
900
+ it('Hockey at halftime should NOT mark any periods as complete (period-based, no halftime concept)', () => {
901
+ const event = {
902
+ sport: {
903
+ id: 'hockey',
904
+ name: 'Hockey',
905
+ },
906
+ fixture: {
907
+ id: 'nhl-halftime-202',
908
+ status: 'half',
909
+ is_live: true,
910
+ },
911
+ scores: {
912
+ home: {
913
+ total: 2.0,
914
+ periods: {
915
+ period_1: 1.0,
916
+ period_2: 1.0,
917
+ },
918
+ },
919
+ away: {
920
+ total: 1.0,
921
+ periods: {
922
+ period_1: 0.0,
923
+ period_2: 1.0,
924
+ },
925
+ },
926
+ },
927
+ in_play: {
928
+ period: 'half',
929
+ },
930
+ };
931
+
932
+ const result = detectCompletedPeriods(event);
933
+
934
+ // Hockey doesn't have traditional halftime, so halftime status shouldn't mark periods complete
935
+ // Periods should only be marked complete based on in_play.period progression
936
+ expect(result).toBeNull();
937
+ });
938
+
939
+ it('Unknown sport at halftime should default to PERIOD_BASED (no halftime processing)', () => {
940
+ const event = {
941
+ sport: {
942
+ id: 'unknown_sport',
943
+ name: 'Unknown Sport',
944
+ },
945
+ fixture: {
946
+ id: 'unknown-halftime-123',
947
+ status: 'half',
948
+ is_live: true,
949
+ },
950
+ scores: {
951
+ home: {
952
+ total: 50.0,
953
+ periods: {
954
+ period_1: 25.0,
955
+ period_2: 25.0,
956
+ },
957
+ },
958
+ away: {
959
+ total: 40.0,
960
+ periods: {
961
+ period_1: 20.0,
962
+ period_2: 20.0,
963
+ },
964
+ },
965
+ },
966
+ in_play: {
967
+ period: 'half',
968
+ },
969
+ };
970
+
971
+ const result = detectCompletedPeriods(event);
972
+
973
+ // Unknown sports default to PERIOD_BASED (no halftime processing)
974
+ // So halftime status should NOT mark any periods as complete
975
+ expect(result).toBeNull();
976
+ });
977
+
978
+ it('Event with missing sport.id should default to PERIOD_BASED (no halftime processing)', () => {
979
+ const event = {
980
+ sport: {
981
+ name: 'Some Sport',
982
+ },
983
+ fixture: {
984
+ id: 'no-sport-id-123',
985
+ status: 'half',
986
+ is_live: true,
987
+ },
988
+ scores: {
989
+ home: {
990
+ total: 50.0,
991
+ periods: {
992
+ period_1: 25.0,
993
+ period_2: 25.0,
994
+ },
995
+ },
996
+ away: {
997
+ total: 40.0,
998
+ periods: {
999
+ period_1: 20.0,
1000
+ period_2: 20.0,
1001
+ },
1002
+ },
1003
+ },
1004
+ in_play: {
1005
+ period: 'half',
1006
+ },
1007
+ };
1008
+
1009
+ const result = detectCompletedPeriods(event);
1010
+
1011
+ // Missing sport.id defaults to PERIOD_BASED (no halftime processing)
1012
+ expect(result).toBeNull();
1013
+ });
1014
+ });
1015
+
1016
+ describe('canResolveMarketsForEvent', () => {
1017
+ describe('Single typeId checks', () => {
1018
+ it('Should return true for 1st period typeId when period 1 is complete (Soccer 2nd half)', () => {
1019
+ const result = canResolveMarketsForEvent(MockSoccerLiveSecondHalf, 10021, SportPeriodType.HALVES_BASED);
1020
+ expect(result).toBe(true);
1021
+ });
1022
+
1023
+ it('Should return false for 2nd period typeId when only period 1 is complete', () => {
1024
+ const result = canResolveMarketsForEvent(MockSoccerLiveSecondHalf, 10022, SportPeriodType.HALVES_BASED);
1025
+ expect(result).toBe(false);
1026
+ });
1027
+
1028
+ it('Should return false for full game typeId during live game', () => {
1029
+ const result = canResolveMarketsForEvent(MockSoccerLiveSecondHalf, 10001, SportPeriodType.HALVES_BASED);
1030
+ expect(result).toBe(false);
1031
+ });
1032
+
1033
+ it('Should return true for full game typeId when game is completed', () => {
1034
+ const result = canResolveMarketsForEvent(MockSoccerCompletedEvent, 10001, SportPeriodType.HALVES_BASED);
1035
+ expect(result).toBe(true);
1036
+ });
1037
+
1038
+ it('Should return true for 1st quarter typeId when quarter 1 complete (NFL)', () => {
1039
+ const result = canResolveMarketsForEvent(
1040
+ MockNFLLiveThirdQuarter,
1041
+ 10021,
1042
+ SportPeriodType.QUARTERS_BASED
1043
+ );
1044
+ expect(result).toBe(true);
1045
+ });
1046
+
1047
+ it('Should return true for 2nd quarter typeId when quarters 1-2 complete (NFL)', () => {
1048
+ const result = canResolveMarketsForEvent(
1049
+ MockNFLLiveThirdQuarter,
1050
+ 10022,
1051
+ SportPeriodType.QUARTERS_BASED
1052
+ );
1053
+ expect(result).toBe(true);
1054
+ });
1055
+
1056
+ it('Should return false for 3rd quarter typeId during 3rd quarter (NFL)', () => {
1057
+ const result = canResolveMarketsForEvent(
1058
+ MockNFLLiveThirdQuarter,
1059
+ 10023,
1060
+ SportPeriodType.QUARTERS_BASED
1061
+ );
1062
+ expect(result).toBe(false);
1063
+ });
1064
+
1065
+ it('Should return true for all quarter typeIds when game is completed (NFL)', () => {
1066
+ expect(canResolveMarketsForEvent(MockNFLCompletedEvent, 10021, SportPeriodType.QUARTERS_BASED)).toBe(
1067
+ true
1068
+ );
1069
+ expect(canResolveMarketsForEvent(MockNFLCompletedEvent, 10022, SportPeriodType.QUARTERS_BASED)).toBe(
1070
+ true
1071
+ );
1072
+ expect(canResolveMarketsForEvent(MockNFLCompletedEvent, 10023, SportPeriodType.QUARTERS_BASED)).toBe(
1073
+ true
1074
+ );
1075
+ expect(canResolveMarketsForEvent(MockNFLCompletedEvent, 10024, SportPeriodType.QUARTERS_BASED)).toBe(
1076
+ true
1077
+ );
1078
+ });
1079
+
1080
+ it('Should return false when no periods are complete', () => {
1081
+ const result = canResolveMarketsForEvent(MockSoccerLiveFirstHalf, 10021, SportPeriodType.HALVES_BASED);
1082
+ expect(result).toBe(false);
1083
+ });
1084
+
1085
+ it('Should return false for non-existent typeId', () => {
1086
+ const result = canResolveMarketsForEvent(MockSoccerLiveSecondHalf, 99999, SportPeriodType.HALVES_BASED);
1087
+ expect(result).toBe(false);
1088
+ });
1089
+ });
1090
+
1091
+ describe('Batch typeIds checks', () => {
1092
+ it('Should return only resolvable typeIds for live soccer in 2nd half', () => {
1093
+ const typeIds = [10021, 10022, 10031, 10001];
1094
+ const result = filterMarketsThatCanBeResolved(
1095
+ MockSoccerLiveSecondHalf,
1096
+ typeIds,
1097
+ SportPeriodType.HALVES_BASED
1098
+ );
1099
+
1100
+ expect(result).toEqual([10021, 10031]); // Only period 1 typeIds
1101
+ });
1102
+
1103
+ it('Should exclude full game typeIds during live game', () => {
1104
+ const typeIds = [10021, 10001, 10002, 10003];
1105
+ const result = filterMarketsThatCanBeResolved(
1106
+ MockNFLLiveThirdQuarter,
1107
+ typeIds,
1108
+ SportPeriodType.QUARTERS_BASED
1109
+ );
1110
+
1111
+ expect(result).toEqual([10021]); // Full game typeIds excluded
1112
+ });
1113
+
1114
+ it('Should include full game typeIds when game is completed', () => {
1115
+ const typeIds = [10021, 10022, 10001, 10002];
1116
+ const result = filterMarketsThatCanBeResolved(
1117
+ MockSoccerCompletedEvent,
1118
+ typeIds,
1119
+ SportPeriodType.HALVES_BASED
1120
+ );
1121
+
1122
+ expect(result).toEqual([10021, 10022, 10001, 10002]);
1123
+ });
1124
+
1125
+ it('Should return empty array when no typeIds are resolvable', () => {
1126
+ const typeIds = [10022, 10023, 10024];
1127
+ const result = filterMarketsThatCanBeResolved(
1128
+ MockSoccerLiveSecondHalf,
1129
+ typeIds,
1130
+ SportPeriodType.HALVES_BASED
1131
+ );
1132
+
1133
+ expect(result).toEqual([]);
1134
+ });
1135
+
1136
+ it('Should return multiple period typeIds for NFL game in 3rd quarter', () => {
1137
+ const typeIds = [10021, 10022, 10023, 10024, 10031, 10032, 10051];
1138
+ const result = filterMarketsThatCanBeResolved(
1139
+ MockNFLLiveThirdQuarter,
1140
+ typeIds,
1141
+ SportPeriodType.QUARTERS_BASED
1142
+ );
1143
+
1144
+ // Periods 1 and 2 are complete (period 2 also completes 1st half = 10051)
1145
+ expect(result).toEqual([10021, 10022, 10031, 10032, 10051]);
1146
+ });
1147
+
1148
+ it('Should handle all 9 periods for completed MLB game', () => {
1149
+ const typeIds = [10021, 10022, 10023, 10024, 10025, 10026, 10027, 10028, 10029];
1150
+ const result = filterMarketsThatCanBeResolved(
1151
+ MockMLBCompletedEvent,
1152
+ typeIds,
1153
+ SportPeriodType.INNINGS_BASED
1154
+ );
1155
+
1156
+ expect(result).toEqual(typeIds); // All 9 innings complete
1157
+ });
1158
+
1159
+ it('Should return empty array when no periods complete', () => {
1160
+ const typeIds = [10021, 10022, 10031];
1161
+ const result = filterMarketsThatCanBeResolved(
1162
+ MockSoccerLiveFirstHalf,
1163
+ typeIds,
1164
+ SportPeriodType.HALVES_BASED
1165
+ );
1166
+
1167
+ expect(result).toEqual([]);
1168
+ });
1169
+ });
1170
+
1171
+ describe('Multiple typeIds boolean array checks', () => {
1172
+ it('Should return boolean array for live soccer in 2nd half', () => {
1173
+ const typeIds = [10021, 10022, 10031, 10001];
1174
+ const result = canResolveMultipleTypeIdsForEvent(
1175
+ MockSoccerLiveSecondHalf,
1176
+ typeIds,
1177
+ SportPeriodType.HALVES_BASED
1178
+ );
1179
+
1180
+ expect(result).toEqual([true, false, true, false]); // Period 1 typeIds are true, period 2 and full game are false
1181
+ });
1182
+
1183
+ it('Should return false for full game typeIds during live game', () => {
1184
+ const typeIds = [10021, 10001, 10002, 10003];
1185
+ const result = canResolveMultipleTypeIdsForEvent(
1186
+ MockNFLLiveThirdQuarter,
1187
+ typeIds,
1188
+ SportPeriodType.QUARTERS_BASED
1189
+ );
1190
+
1191
+ expect(result).toEqual([true, false, false, false]); // Only period 1 is true
1192
+ });
1193
+
1194
+ it('Should return all true for completed game', () => {
1195
+ const typeIds = [10021, 10022, 10001, 10002];
1196
+ const result = canResolveMultipleTypeIdsForEvent(
1197
+ MockSoccerCompletedEvent,
1198
+ typeIds,
1199
+ SportPeriodType.HALVES_BASED
1200
+ );
1201
+
1202
+ expect(result).toEqual([true, true, true, true]); // All complete
1203
+ });
1204
+
1205
+ it('Should return all false when no typeIds are resolvable', () => {
1206
+ const typeIds = [10022, 10023, 10024];
1207
+ const result = canResolveMultipleTypeIdsForEvent(
1208
+ MockSoccerLiveSecondHalf,
1209
+ typeIds,
1210
+ SportPeriodType.HALVES_BASED
1211
+ );
1212
+
1213
+ expect(result).toEqual([false, false, false]);
1214
+ });
1215
+
1216
+ it('Should return mixed booleans for NFL game in 3rd quarter', () => {
1217
+ const typeIds = [10021, 10022, 10023, 10024, 10031, 10032, 10051];
1218
+ const result = canResolveMultipleTypeIdsForEvent(
1219
+ MockNFLLiveThirdQuarter,
1220
+ typeIds,
1221
+ SportPeriodType.QUARTERS_BASED
1222
+ );
1223
+
1224
+ // Periods 1 and 2 are complete (period 2 also completes 1st half = 10051)
1225
+ expect(result).toEqual([true, true, false, false, true, true, true]);
1226
+ });
1227
+
1228
+ it('Should handle all 9 periods for completed MLB game', () => {
1229
+ const typeIds = [10021, 10022, 10023, 10024, 10025, 10026, 10027, 10028, 10029];
1230
+ const result = canResolveMultipleTypeIdsForEvent(
1231
+ MockMLBCompletedEvent,
1232
+ typeIds,
1233
+ SportPeriodType.INNINGS_BASED
1234
+ );
1235
+
1236
+ expect(result).toEqual([true, true, true, true, true, true, true, true, true]); // All 9 innings complete
1237
+ });
1238
+
1239
+ it('Should return all false when no periods complete', () => {
1240
+ const typeIds = [10021, 10022, 10031];
1241
+ const result = canResolveMultipleTypeIdsForEvent(
1242
+ MockSoccerLiveFirstHalf,
1243
+ typeIds,
1244
+ SportPeriodType.HALVES_BASED
1245
+ );
1246
+
1247
+ expect(result).toEqual([false, false, false]);
1248
+ });
1249
+
1250
+ it('Should work with numeric sport type parameter', () => {
1251
+ const typeIds = [10021, 10022];
1252
+ const result = canResolveMultipleTypeIdsForEvent(MockSoccerLiveSecondHalf, typeIds, 0); // 0 = HALVES_BASED
1253
+
1254
+ expect(result).toEqual([true, false]);
1255
+ });
1256
+ });
1257
+
1258
+ describe('Edge cases', () => {
1259
+ it('Should handle event with no completed periods', () => {
1260
+ const result = canResolveMarketsForEvent(
1261
+ MockSoccerLiveFirstHalfInProgress,
1262
+ 10021,
1263
+ SportPeriodType.HALVES_BASED
1264
+ );
1265
+ expect(result).toBe(false);
1266
+ });
1267
+
1268
+ it('Should respect full game typeIds list', () => {
1269
+ // Test all full game typeIds
1270
+ const fullGameTypeIds = [0, 10001, 10002, 10003, 10004, 10010, 10011, 10012];
1271
+
1272
+ fullGameTypeIds.forEach((typeId) => {
1273
+ const result = canResolveMarketsForEvent(
1274
+ MockNFLLiveThirdQuarter,
1275
+ typeId,
1276
+ SportPeriodType.QUARTERS_BASED
1277
+ );
1278
+ expect(result).toBe(false);
1279
+ });
1280
+ });
1281
+
1282
+ it('Should work with sport type parameter for single typeId', () => {
1283
+ const result = canResolveMarketsForEvent(MockSoccerLiveSecondHalf, 10021, SportPeriodType.HALVES_BASED);
1284
+ expect(result).toBe(true);
1285
+ });
1286
+
1287
+ it('Should work with sport type parameter for batch typeIds', () => {
1288
+ const result = filterMarketsThatCanBeResolved(
1289
+ MockSoccerLiveSecondHalf,
1290
+ [10021, 10022],
1291
+ SportPeriodType.HALVES_BASED
1292
+ );
1293
+ expect(result).toEqual([10021]);
1294
+ });
1295
+ });
1296
+
1297
+ describe('Real overtime NFL game (Rams vs 49ers)', () => {
1298
+ it('Should detect all 5 periods complete including overtime', () => {
1299
+ const result = detectCompletedPeriods(MockNFLCompletedWithOvertime);
1300
+
1301
+ expect(result).not.toBeNull();
1302
+ expect(result?.completedPeriods).toEqual([1, 2, 3, 4, 5]);
1303
+ expect(result?.readyForResolution).toBe(true);
1304
+ expect(result?.periodScores['period5']).toEqual({ home: 0.0, away: 3.0 });
1305
+ });
1306
+
1307
+ it('Should resolve all quarter typeIds (10021-10024) for completed overtime game', () => {
1308
+ expect(
1309
+ canResolveMarketsForEvent(MockNFLCompletedWithOvertime, 10021, SportPeriodType.QUARTERS_BASED)
1310
+ ).toBe(true);
1311
+ expect(
1312
+ canResolveMarketsForEvent(MockNFLCompletedWithOvertime, 10022, SportPeriodType.QUARTERS_BASED)
1313
+ ).toBe(true);
1314
+ expect(
1315
+ canResolveMarketsForEvent(MockNFLCompletedWithOvertime, 10023, SportPeriodType.QUARTERS_BASED)
1316
+ ).toBe(true);
1317
+ expect(
1318
+ canResolveMarketsForEvent(MockNFLCompletedWithOvertime, 10024, SportPeriodType.QUARTERS_BASED)
1319
+ ).toBe(true);
1320
+ });
1321
+
1322
+ it('Should NOT resolve overtime period typeId (10025) - overtime has no specific markets', () => {
1323
+ const result = canResolveMarketsForEvent(
1324
+ MockNFLCompletedWithOvertime,
1325
+ 10025,
1326
+ SportPeriodType.QUARTERS_BASED
1327
+ );
1328
+ expect(result).toBe(false);
1329
+ });
1330
+
1331
+ it('Should resolve full game typeIds for completed overtime game', () => {
1332
+ expect(
1333
+ canResolveMarketsForEvent(MockNFLCompletedWithOvertime, 10001, SportPeriodType.QUARTERS_BASED)
1334
+ ).toBe(true);
1335
+ expect(
1336
+ canResolveMarketsForEvent(MockNFLCompletedWithOvertime, 10002, SportPeriodType.QUARTERS_BASED)
1337
+ ).toBe(true);
1338
+ });
1339
+
1340
+ it('Should return all resolvable typeIds excluding overtime in batch check', () => {
1341
+ const typeIds = [10021, 10022, 10023, 10024, 10025, 10001];
1342
+ const result = filterMarketsThatCanBeResolved(
1343
+ MockNFLCompletedWithOvertime,
1344
+ typeIds,
1345
+ SportPeriodType.QUARTERS_BASED
1346
+ );
1347
+
1348
+ // 10025 (overtime) should not be included as period 5 has no specific markets
1349
+ expect(result).toEqual([10021, 10022, 10023, 10024, 10001]);
1350
+ });
1351
+
1352
+ it('Should return false for 8th period typeId (period did not occur)', () => {
1353
+ const result = canResolveMarketsForEvent(
1354
+ MockNFLCompletedWithOvertime,
1355
+ 10028,
1356
+ SportPeriodType.QUARTERS_BASED
1357
+ );
1358
+ expect(result).toBe(false);
1359
+ });
1360
+
1361
+ it('Should return false for 9th period typeId (period did not occur)', () => {
1362
+ const result = canResolveMarketsForEvent(
1363
+ MockNFLCompletedWithOvertime,
1364
+ 10029,
1365
+ SportPeriodType.QUARTERS_BASED
1366
+ );
1367
+ expect(result).toBe(false);
1368
+ });
1369
+
1370
+ it('Should not include non-existent periods in batch check', () => {
1371
+ const typeIds = [10021, 10022, 10023, 10024, 10025, 10028, 10029];
1372
+ const result = filterMarketsThatCanBeResolved(
1373
+ MockNFLCompletedWithOvertime,
1374
+ typeIds,
1375
+ SportPeriodType.QUARTERS_BASED
1376
+ );
1377
+
1378
+ // Periods 1-4 are resolvable; period 5 (overtime) has no markets; periods 8-9 did not occur
1379
+ expect(result).toEqual([10021, 10022, 10023, 10024]);
1380
+ });
1381
+ });
1382
+
1383
+ describe('Sport-type-specific resolution for typeId 10051 (1st half)', () => {
1384
+ it('Soccer (HALVES_BASED): Should resolve typeId 10051 after period 1', () => {
1385
+ // Soccer: Period 1 = 1st half
1386
+ const result = canResolveMarketsForEvent(MockSoccerLiveSecondHalf, 10051, SportPeriodType.HALVES_BASED);
1387
+ expect(result).toBe(true);
1388
+ });
1389
+
1390
+ it('NFL (QUARTERS_BASED): Should resolve typeId 10051 after period 2', () => {
1391
+ // NFL: Period 2 completes 1st half (quarters 1+2)
1392
+ const result = canResolveMarketsForEvent(
1393
+ MockNFLLiveThirdQuarter,
1394
+ 10051,
1395
+ SportPeriodType.QUARTERS_BASED
1396
+ );
1397
+ expect(result).toBe(true);
1398
+ });
1399
+
1400
+ it('NFL (QUARTERS_BASED): Should NOT resolve typeId 10051 after only period 1', () => {
1401
+ // Create mock with only period 1 complete
1402
+ const mockNFLFirstQuarter = {
1403
+ ...MockNFLLiveThirdQuarter,
1404
+ scores: {
1405
+ home: { total: 7.0, periods: { period_1: 7.0 } },
1406
+ away: { total: 3.0, periods: { period_1: 3.0 } },
1407
+ },
1408
+ in_play: { period: '2', clock: '5:00' },
1409
+ };
1410
+
1411
+ const result = canResolveMarketsForEvent(mockNFLFirstQuarter, 10051, SportPeriodType.QUARTERS_BASED);
1412
+ expect(result).toBe(false);
1413
+ });
1414
+
1415
+ it('MLB (INNINGS_BASED): Should resolve typeId 10051 after period 5', () => {
1416
+ // MLB: Period 5 completes 1st half (innings 1-5)
1417
+ const result = canResolveMarketsForEvent(MockMLBLiveSixthInning, 10051, SportPeriodType.INNINGS_BASED);
1418
+ expect(result).toBe(true);
1419
+ });
1420
+
1421
+ it('MLB (INNINGS_BASED): Should NOT resolve typeId 10051 after only period 4', () => {
1422
+ // Create mock with only period 1-4 complete
1423
+ const mockMLBFourthInning = {
1424
+ ...MockMLBLiveSixthInning,
1425
+ scores: {
1426
+ home: {
1427
+ total: 3.0,
1428
+ periods: {
1429
+ period_1: 0.0,
1430
+ period_2: 2.0,
1431
+ period_3: 1.0,
1432
+ period_4: 0.0,
1433
+ },
1434
+ },
1435
+ away: {
1436
+ total: 1.0,
1437
+ periods: {
1438
+ period_1: 1.0,
1439
+ period_2: 0.0,
1440
+ period_3: 0.0,
1441
+ period_4: 0.0,
1442
+ },
1443
+ },
1444
+ },
1445
+ in_play: { period: '5', clock: null },
1446
+ };
1447
+
1448
+ const result = canResolveMarketsForEvent(mockMLBFourthInning, 10051, SportPeriodType.INNINGS_BASED);
1449
+ expect(result).toBe(false);
1450
+ });
1451
+ });
1452
+
1453
+ describe('Sport-type-specific resolution for typeId 10052 (2nd half)', () => {
1454
+ it('Soccer (HALVES_BASED): Should resolve typeId 10052 after period 2', () => {
1455
+ const result = canResolveMarketsForEvent(MockSoccerCompletedEvent, 10052, SportPeriodType.HALVES_BASED);
1456
+ expect(result).toBe(true);
1457
+ });
1458
+
1459
+ it('NFL (QUARTERS_BASED): Should resolve typeId 10052 after period 4', () => {
1460
+ const result = canResolveMarketsForEvent(MockNFLCompletedEvent, 10052, SportPeriodType.QUARTERS_BASED);
1461
+ expect(result).toBe(true);
1462
+ });
1463
+
1464
+ it('MLB (INNINGS_BASED): Should resolve typeId 10052 after period 9', () => {
1465
+ const result = canResolveMarketsForEvent(MockMLBCompletedEvent, 10052, SportPeriodType.INNINGS_BASED);
1466
+ expect(result).toBe(true);
1467
+ });
1468
+ });
1469
+
1470
+ describe('Error handling for invalid sport type numbers', () => {
1471
+ it('Should throw error for invalid sport type number when using numeric parameter', () => {
1472
+ const typeIds = [10021, 10022];
1473
+
1474
+ // Invalid sport type number (must be 0-3)
1475
+ expect(() => {
1476
+ canResolveMultipleTypeIdsForEvent(MockSoccerLiveSecondHalf, typeIds, 99);
1477
+ }).toThrow('Invalid sport type number: 99. Must be 0 (halves), 1 (quarters), 2 (innings), or 3 (period).');
1478
+ });
1479
+
1480
+ it('Should throw error for negative sport type number', () => {
1481
+ const typeIds = [10021];
1482
+
1483
+ expect(() => {
1484
+ canResolveMultipleTypeIdsForEvent(MockSoccerLiveSecondHalf, typeIds, -1);
1485
+ }).toThrow('Invalid sport type number: -1. Must be 0 (halves), 1 (quarters), 2 (innings), or 3 (period).');
1486
+ });
1487
+ });
1488
+ });
1489
+ });