diff --git a/apps/admin/src/components/LeagueRowActions.vue b/apps/admin/src/components/LeagueRowActions.vue index f1f24bd..842e94f 100644 --- a/apps/admin/src/components/LeagueRowActions.vue +++ b/apps/admin/src/components/LeagueRowActions.vue @@ -49,7 +49,7 @@ defineEmits<{ {{ row.labels.publish }} ).filter( (m) => m.isHot && m.status !== 'SETTLED', ); - const todayMatches = (allMatches as Array<{ startTime: string }>).filter((m) => { + const todayMatches = (allMatches as Array<{ startTime: string; status?: string }>).filter((m) => { + if (m.status === 'SETTLED') return false; const kickoff = new Date(m.startTime); return !Number.isNaN(kickoff.getTime()) && isInLocalTodayMatchWindow(kickoff, now, timeZone); }); diff --git a/apps/api/src/domains/catalog/matches.service.spec.ts b/apps/api/src/domains/catalog/matches.service.spec.ts index 51f2fab..ef687f5 100644 --- a/apps/api/src/domains/catalog/matches.service.spec.ts +++ b/apps/api/src/domains/catalog/matches.service.spec.ts @@ -267,6 +267,15 @@ describe('MatchesService listUpcomingPublished', () => { sportType: 'FOOTBALL', deletedAt: null, startTime: { gte: now, lte: end }, + league: { + isActive: true, + deletedAt: null, + NOT: { + matches: { + some: { isOutright: true, status: 'SETTLED', deletedAt: null }, + }, + }, + }, }), orderBy: [{ startTime: 'asc' }, { displayOrder: 'asc' }], take: 50, @@ -302,6 +311,52 @@ describe('MatchesService listUpcomingPublished', () => { }); }); +describe('MatchesService listPublished', () => { + let prisma: { + match: { findMany: jest.Mock }; + entityTranslation: { findMany: jest.Mock }; + }; + let service: MatchesService; + + beforeEach(() => { + prisma = { + match: { findMany: jest.fn().mockResolvedValue([]) }, + entityTranslation: { + findMany: jest.fn().mockResolvedValue([]), + }, + }; + service = new MatchesService( + prisma as never, + { syncWithLeaguePublished: jest.fn() } as never, + { betStatsForMatches: jest.fn().mockResolvedValue(new Map()) } as never, + ); + }); + + it('excludes settled fixtures and leagues with settled outright', async () => { + await service.listPublished('zh-CN', undefined, { includeMarkets: false }); + + expect(prisma.match.findMany).toHaveBeenCalledWith( + expect.objectContaining({ + where: { + status: { in: ['PUBLISHED', 'CLOSED', 'PENDING_SETTLEMENT'] }, + isOutright: false, + sportType: 'FOOTBALL', + deletedAt: null, + league: { + isActive: true, + deletedAt: null, + NOT: { + matches: { + some: { isOutright: true, status: 'SETTLED', deletedAt: null }, + }, + }, + }, + }, + }), + ); + }); +}); + describe('MatchesService listAdminLeagueMatches', () => { const leagueId = BigInt(1); diff --git a/apps/api/src/domains/catalog/matches.service.ts b/apps/api/src/domains/catalog/matches.service.ts index 9046628..e0139e1 100644 --- a/apps/api/src/domains/catalog/matches.service.ts +++ b/apps/api/src/domains/catalog/matches.service.ts @@ -1554,11 +1554,19 @@ export class MatchesService { const matches = await this.prisma.match.findMany({ where: { - status: { in: ['PUBLISHED', 'CLOSED', 'PENDING_SETTLEMENT', 'SETTLED'] }, + status: { in: ['PUBLISHED', 'CLOSED', 'PENDING_SETTLEMENT'] }, isOutright: false, sportType: 'FOOTBALL', deletedAt: null, - league: { isActive: true, deletedAt: null }, + league: { + isActive: true, + deletedAt: null, + NOT: { + matches: { + some: { isOutright: true, status: 'SETTLED', deletedAt: null }, + }, + }, + }, ...(leagueId ? { leagueId } : {}), }, include: { @@ -1602,7 +1610,15 @@ export class MatchesService { sportType: 'FOOTBALL', deletedAt: null, startTime: { gte: now, lte: end }, - league: { isActive: true, deletedAt: null }, + league: { + isActive: true, + deletedAt: null, + NOT: { + matches: { + some: { isOutright: true, status: 'SETTLED', deletedAt: null }, + }, + }, + }, }, include: { league: true, diff --git a/apps/api/src/domains/catalog/outright.service.spec.ts b/apps/api/src/domains/catalog/outright.service.spec.ts new file mode 100644 index 0000000..cff2864 --- /dev/null +++ b/apps/api/src/domains/catalog/outright.service.spec.ts @@ -0,0 +1,33 @@ +jest.mock('./wc2026-outright.sync', () => ({ + syncWc2026OutrightMarket: jest.fn().mockResolvedValue(undefined), +})); + +import { OutrightService } from './outright.service'; + +describe('OutrightService listForPlayer', () => { + let prisma: { match: { findMany: jest.Mock } }; + let service: OutrightService; + + beforeEach(() => { + prisma = { + match: { findMany: jest.fn().mockResolvedValue([]) }, + }; + service = new OutrightService(prisma as never, {} as never); + }); + + it('returns only published outright markets for player browse', async () => { + await service.listForPlayer('zh-CN'); + + expect(prisma.match.findMany).toHaveBeenCalledWith( + expect.objectContaining({ + where: expect.objectContaining({ + status: 'PUBLISHED', + isOutright: true, + sportType: 'FOOTBALL', + deletedAt: null, + league: { isActive: true, deletedAt: null }, + }), + }), + ); + }); +}); diff --git a/apps/api/src/domains/catalog/outright.service.ts b/apps/api/src/domains/catalog/outright.service.ts index 96274e8..cbcf5b2 100644 --- a/apps/api/src/domains/catalog/outright.service.ts +++ b/apps/api/src/domains/catalog/outright.service.ts @@ -701,7 +701,7 @@ export class OutrightService { const matches = await this.prisma.match.findMany({ where: { - status: { in: ['PUBLISHED', 'SETTLED'] }, + status: 'PUBLISHED', isOutright: true, sportType: 'FOOTBALL', deletedAt: null,