refactor: update match time utilities and improve local match window logic
- Introduced new functions `isInLocalTodayMatchWindow` and `isAfterLocalTodayMatchWindow` to enhance match time checks. - Replaced the deprecated `dayKeyInTimeZone` function with `isInLocalTodayMatchWindow` in the PlayerController. - Updated related tests and utility exports to reflect the new naming conventions. - Improved handling of local match windows in various components for better accuracy.
This commit is contained in:
@@ -30,6 +30,7 @@ import { BetsService } from '../../domains/betting/bets.service';
|
|||||||
import { ContentService } from '../../domains/operations/content/content.service';
|
import { ContentService } from '../../domains/operations/content/content.service';
|
||||||
import { CashbackService } from '../../domains/operations/cashback/cashback.service';
|
import { CashbackService } from '../../domains/operations/cashback/cashback.service';
|
||||||
import { DepositService } from '../../domains/deposit/deposit.service';
|
import { DepositService } from '../../domains/deposit/deposit.service';
|
||||||
|
import { isInLocalTodayMatchWindow } from '@thebet365/shared';
|
||||||
import { IsString, IsNumber, IsArray, ValidateNested, Min, IsOptional } from 'class-validator';
|
import { IsString, IsNumber, IsArray, ValidateNested, Min, IsOptional } from 'class-validator';
|
||||||
import { Type } from 'class-transformer';
|
import { Type } from 'class-transformer';
|
||||||
|
|
||||||
@@ -104,17 +105,6 @@ function safeTimeZone(input?: string): string {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function dayKeyInTimeZone(date: Date, timeZone: string): string {
|
|
||||||
const parts = new Intl.DateTimeFormat('en-US', {
|
|
||||||
timeZone,
|
|
||||||
year: 'numeric',
|
|
||||||
month: '2-digit',
|
|
||||||
day: '2-digit',
|
|
||||||
}).formatToParts(date);
|
|
||||||
const map = new Map(parts.map((part) => [part.type, part.value]));
|
|
||||||
return `${map.get('year')}-${map.get('month')}-${map.get('day')}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
@ApiTags('Player')
|
@ApiTags('Player')
|
||||||
@Controller('player')
|
@Controller('player')
|
||||||
@UseGuards(JwtAuthGuard, PlayerGuard)
|
@UseGuards(JwtAuthGuard, PlayerGuard)
|
||||||
@@ -191,11 +181,11 @@ export class PlayerController {
|
|||||||
this.matches.listPublished(locale, undefined, { includeMarkets: false }),
|
this.matches.listPublished(locale, undefined, { includeMarkets: false }),
|
||||||
]);
|
]);
|
||||||
const timeZone = safeTimeZone(headerTimeZone);
|
const timeZone = safeTimeZone(headerTimeZone);
|
||||||
const todayKey = dayKeyInTimeZone(new Date(), timeZone);
|
const now = new Date();
|
||||||
const hotMatches = (allMatches as Array<{ isHot?: boolean }>).filter((m) => m.isHot);
|
const hotMatches = (allMatches as Array<{ isHot?: boolean }>).filter((m) => m.isHot);
|
||||||
const todayMatches = (allMatches as Array<{ startTime: string }>).filter((m) => {
|
const todayMatches = (allMatches as Array<{ startTime: string }>).filter((m) => {
|
||||||
const kickoff = new Date(m.startTime);
|
const kickoff = new Date(m.startTime);
|
||||||
return !Number.isNaN(kickoff.getTime()) && dayKeyInTimeZone(kickoff, timeZone) === todayKey;
|
return !Number.isNaN(kickoff.getTime()) && isInLocalTodayMatchWindow(kickoff, now, timeZone);
|
||||||
});
|
});
|
||||||
return jsonResponse({
|
return jsonResponse({
|
||||||
banners,
|
banners,
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import {
|
import {
|
||||||
formatLocalMatchDateTime,
|
formatLocalMatchDateTime,
|
||||||
isAfterLocalToday,
|
isAfterLocalTodayMatchWindow,
|
||||||
isInLocalToday,
|
isInLocalTodayMatchWindow,
|
||||||
isoToPlatformPickerDateTime,
|
isoToPlatformPickerDateTime,
|
||||||
platformPickerDateTimeToIso,
|
platformPickerDateTimeToIso,
|
||||||
} from '@thebet365/shared';
|
} from '@thebet365/shared';
|
||||||
@@ -32,10 +32,17 @@ describe('match time helpers', () => {
|
|||||||
expect(malaysia).not.toBe(newYork);
|
expect(malaysia).not.toBe(newYork);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('uses the player local day for today and early buckets', () => {
|
it('keeps early next-day matches in the player today bucket until local noon', () => {
|
||||||
const now = new Date('2026-06-11T15:00:00-04:00');
|
const now = new Date('2026-06-11T15:00:00-04:00');
|
||||||
|
|
||||||
expect(isInLocalToday('2026-06-12T02:00:00.000Z', now, 'America/New_York')).toBe(true);
|
expect(isInLocalTodayMatchWindow('2026-06-12T02:00:00.000Z', now, 'America/New_York')).toBe(
|
||||||
expect(isAfterLocalToday('2026-06-12T05:00:00.000Z', now, 'America/New_York')).toBe(true);
|
true,
|
||||||
|
);
|
||||||
|
expect(isInLocalTodayMatchWindow('2026-06-12T15:59:59.000Z', now, 'America/New_York')).toBe(
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
expect(isAfterLocalTodayMatchWindow('2026-06-12T16:00:00.000Z', now, 'America/New_York')).toBe(
|
||||||
|
true,
|
||||||
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
export {
|
export {
|
||||||
isAfterLocalToday as isAfterTodayMatchWindow,
|
isAfterLocalTodayMatchWindow as isAfterTodayMatchWindow,
|
||||||
isInLocalToday as isInTodayMatchWindow,
|
isInLocalTodayMatchWindow as isInTodayMatchWindow,
|
||||||
isSameLocalCalendarDay as isSameCalendarDay,
|
isSameLocalCalendarDay as isSameCalendarDay,
|
||||||
} from '@thebet365/shared';
|
} from '@thebet365/shared';
|
||||||
|
|||||||
@@ -11,8 +11,8 @@ import GoldSpinner from '../components/GoldSpinner.vue';
|
|||||||
import { usePullToRefresh } from '../composables/usePullToRefresh';
|
import { usePullToRefresh } from '../composables/usePullToRefresh';
|
||||||
import type { MatchPhase } from '../utils/matchPhase';
|
import type { MatchPhase } from '../utils/matchPhase';
|
||||||
import {
|
import {
|
||||||
isAfterLocalToday as isAfterTodayMatchWindow,
|
isAfterLocalTodayMatchWindow as isAfterTodayMatchWindow,
|
||||||
isInLocalToday as isInTodayMatchWindow,
|
isInLocalTodayMatchWindow as isInTodayMatchWindow,
|
||||||
} from '@thebet365/shared';
|
} from '@thebet365/shared';
|
||||||
|
|
||||||
type MainTab = 'matches' | 'outright';
|
type MainTab = 'matches' | 'outright';
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
export const PLATFORM_TIME_ZONE = 'Asia/Kuala_Lumpur';
|
export const PLATFORM_TIME_ZONE = 'Asia/Kuala_Lumpur';
|
||||||
export const PLATFORM_TIME_ZONE_OFFSET_MINUTES = 8 * 60;
|
export const PLATFORM_TIME_ZONE_OFFSET_MINUTES = 8 * 60;
|
||||||
export const PLATFORM_TIME_ZONE_OFFSET_LABEL = 'UTC+8';
|
export const PLATFORM_TIME_ZONE_OFFSET_LABEL = 'UTC+8';
|
||||||
|
export const MATCH_TODAY_NEXT_DAY_CUTOFF_HOUR = 12;
|
||||||
|
|
||||||
const PICKER_DATETIME_RE =
|
const PICKER_DATETIME_RE =
|
||||||
/^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2})(?::(\d{2}))?$/;
|
/^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2})(?::(\d{2}))?$/;
|
||||||
@@ -59,6 +60,63 @@ function offsetMinutesInTimeZone(date: Date, timeZone: string): number {
|
|||||||
return Math.round((asUtc - date.getTime()) / 60000);
|
return Math.round((asUtc - date.getTime()) / 60000);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function zonedWallTimeToUtc(
|
||||||
|
parts: { year: number; month: number; day: number; hour: number; minute?: number; second?: number },
|
||||||
|
timeZone: string,
|
||||||
|
): Date {
|
||||||
|
const wallMs = Date.UTC(
|
||||||
|
parts.year,
|
||||||
|
parts.month - 1,
|
||||||
|
parts.day,
|
||||||
|
parts.hour,
|
||||||
|
parts.minute ?? 0,
|
||||||
|
parts.second ?? 0,
|
||||||
|
);
|
||||||
|
let utcMs = wallMs;
|
||||||
|
for (let i = 0; i < 3; i += 1) {
|
||||||
|
utcMs = wallMs - offsetMinutesInTimeZone(new Date(utcMs), timeZone) * 60 * 1000;
|
||||||
|
}
|
||||||
|
return new Date(utcMs);
|
||||||
|
}
|
||||||
|
|
||||||
|
function localDayParts(date: Date, timeZone: string) {
|
||||||
|
const parts = timeZoneParts(date, timeZone);
|
||||||
|
return {
|
||||||
|
year: Number(parts.get('year')),
|
||||||
|
month: Number(parts.get('month')),
|
||||||
|
day: Number(parts.get('day')),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function addDaysToParts(parts: { year: number; month: number; day: number }, days: number) {
|
||||||
|
const d = new Date(Date.UTC(parts.year, parts.month - 1, parts.day + days));
|
||||||
|
return {
|
||||||
|
year: d.getUTCFullYear(),
|
||||||
|
month: d.getUTCMonth() + 1,
|
||||||
|
day: d.getUTCDate(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function localTodayMatchWindow(now = new Date(), timeZone?: string) {
|
||||||
|
if (timeZone) {
|
||||||
|
const today = localDayParts(now, timeZone);
|
||||||
|
const tomorrow = addDaysToParts(today, 1);
|
||||||
|
return {
|
||||||
|
start: zonedWallTimeToUtc({ ...today, hour: 0 }, timeZone),
|
||||||
|
end: zonedWallTimeToUtc(
|
||||||
|
{ ...tomorrow, hour: MATCH_TODAY_NEXT_DAY_CUTOFF_HOUR },
|
||||||
|
timeZone,
|
||||||
|
),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
const start = new Date(now);
|
||||||
|
start.setHours(0, 0, 0, 0);
|
||||||
|
const end = new Date(start);
|
||||||
|
end.setDate(end.getDate() + 1);
|
||||||
|
end.setHours(MATCH_TODAY_NEXT_DAY_CUTOFF_HOUR, 0, 0, 0);
|
||||||
|
return { start, end };
|
||||||
|
}
|
||||||
|
|
||||||
function parsePickerParts(value: string) {
|
function parsePickerParts(value: string) {
|
||||||
const match = PICKER_DATETIME_RE.exec(value.trim());
|
const match = PICKER_DATETIME_RE.exec(value.trim());
|
||||||
if (!match) return null;
|
if (!match) return null;
|
||||||
@@ -190,6 +248,28 @@ export function isAfterLocalToday(value: string | Date, now = new Date(), timeZo
|
|||||||
return date >= end;
|
return date >= end;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function isInLocalTodayMatchWindow(
|
||||||
|
value: string | Date,
|
||||||
|
now = new Date(),
|
||||||
|
timeZone?: string,
|
||||||
|
): boolean {
|
||||||
|
const date = validDate(value);
|
||||||
|
if (!date) return false;
|
||||||
|
const { start, end } = localTodayMatchWindow(now, timeZone);
|
||||||
|
return date >= start && date < end;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isAfterLocalTodayMatchWindow(
|
||||||
|
value: string | Date,
|
||||||
|
now = new Date(),
|
||||||
|
timeZone?: string,
|
||||||
|
): boolean {
|
||||||
|
const date = validDate(value);
|
||||||
|
if (!date) return false;
|
||||||
|
const { end } = localTodayMatchWindow(now, timeZone);
|
||||||
|
return date >= end;
|
||||||
|
}
|
||||||
|
|
||||||
export function formatLocalMatchDateTime(
|
export function formatLocalMatchDateTime(
|
||||||
value: string | Date | null | undefined,
|
value: string | Date | null | undefined,
|
||||||
locale = 'en-US',
|
locale = 'en-US',
|
||||||
|
|||||||
Reference in New Issue
Block a user