feat(theme-3): sync inbox/announcements/presence/deposit/search from main

This commit is contained in:
2026-06-18 10:02:32 +08:00
parent 0d430857c7
commit 6881d325d5
87 changed files with 6944 additions and 964 deletions

View File

@@ -1,10 +1,11 @@
jest.mock('@thebet365/shared', () => ({
isPreMatchKickoff: jest.fn(() => true),
PARLAY_MARKET_TYPES: [],
resolveTranslationFallback: jest.fn(
(translations: Map<string, string>, locale: string) =>
translations.get(locale) ?? translations.get('zh-CN') ?? translations.get('en-US') ?? null,
),
resolveTranslationFallback: jest.fn((translations: Map<string, string> | Record<string, string>, locale: string) => {
const get = (key: string) =>
translations instanceof Map ? translations.get(key) : translations[key];
return get(locale) ?? get('zh-CN') ?? get('en-US') ?? null;
}),
}));
import { MatchesService } from './matches.service';
@@ -18,6 +19,7 @@ describe('MatchesService publish/unpublish', () => {
match: { findFirst: jest.Mock; update: jest.Mock };
entityTranslation: { findFirst: jest.Mock; upsert: jest.Mock };
settlementBatch: { deleteMany: jest.Mock };
marketSelection: { findMany: jest.Mock };
};
let outright: { syncWithLeaguePublished: jest.Mock };
let matchBetStats: { betStatsForMatches: jest.Mock };
@@ -36,6 +38,7 @@ describe('MatchesService publish/unpublish', () => {
},
entityTranslation: { findFirst: jest.fn().mockResolvedValue(null), upsert: jest.fn().mockResolvedValue({}) },
settlementBatch: { deleteMany: jest.fn().mockResolvedValue({ count: 0 }) },
marketSelection: { findMany: jest.fn().mockResolvedValue([]) },
};
outright = { syncWithLeaguePublished: jest.fn().mockResolvedValue(undefined) };
matchBetStats = { betStatsForMatches: jest.fn().mockResolvedValue(new Map()) };
@@ -124,3 +127,177 @@ describe('MatchesService publish/unpublish', () => {
});
});
});
describe('MatchesService getSelectionsOdds', () => {
const selectionId = BigInt(100);
const matchId = BigInt(10);
let prisma: { marketSelection: { findMany: jest.Mock } };
let service: MatchesService;
beforeEach(() => {
prisma = {
marketSelection: { findMany: jest.fn() },
};
service = new MatchesService(
prisma as never,
{ syncWithLeaguePublished: jest.fn() } as never,
{ betStatsForMatches: jest.fn() } as never,
);
});
it('returns odds snapshot for requested selections', async () => {
prisma.marketSelection.findMany.mockResolvedValue([
{
id: selectionId,
odds: { toString: () => '1.95' },
oddsVersion: BigInt(3),
status: 'OPEN',
market: {
status: 'OPEN',
showOnPlayer: true,
match: { id: matchId, status: 'PUBLISHED' },
},
},
]);
const result = await service.getSelectionsOdds([selectionId]);
expect(prisma.marketSelection.findMany).toHaveBeenCalledWith({
where: { id: { in: [selectionId] } },
include: { market: { include: { match: true } } },
});
expect(result).toEqual([
{
id: '100',
odds: '1.95',
oddsVersion: '3',
status: 'OPEN',
marketStatus: 'OPEN',
marketShowOnPlayer: true,
matchStatus: 'PUBLISHED',
matchId: '10',
},
]);
});
it('returns empty array when ids not found', async () => {
prisma.marketSelection.findMany.mockResolvedValue([]);
const result = await service.getSelectionsOdds([BigInt(999)]);
expect(result).toEqual([]);
});
it('returns empty array for empty id list', async () => {
prisma.marketSelection.findMany.mockResolvedValue([]);
const result = await service.getSelectionsOdds([]);
expect(prisma.marketSelection.findMany).toHaveBeenCalledWith({
where: { id: { in: [] } },
include: { market: { include: { match: true } } },
});
expect(result).toEqual([]);
});
});
describe('MatchesService listUpcomingPublished', () => {
const leagueId = BigInt(1);
const homeTeamId = BigInt(2);
const awayTeamId = BigInt(3);
const matchId = BigInt(10);
let prisma: {
match: { findMany: jest.Mock };
entityTranslation: { findMany: jest.Mock };
};
let service: MatchesService;
beforeEach(() => {
jest.useFakeTimers();
jest.setSystemTime(new Date('2026-06-17T12:00:00.000Z'));
prisma = {
match: { findMany: jest.fn() },
entityTranslation: {
findMany: jest.fn().mockResolvedValue([{ locale: 'zh-CN', fieldName: 'name', value: '测试' }]),
},
};
service = new MatchesService(
prisma as never,
{ syncWithLeaguePublished: jest.fn() } as never,
{ betStatsForMatches: jest.fn().mockResolvedValue(new Map()) } as never,
);
});
afterEach(() => {
jest.useRealTimers();
});
it('queries published matches within the next 3 days sorted by startTime', async () => {
const now = new Date('2026-06-17T12:00:00.000Z');
const end = new Date(now);
end.setDate(end.getDate() + 3);
prisma.match.findMany.mockResolvedValue([
{
id: matchId,
leagueId,
homeTeamId,
awayTeamId,
startTime: new Date('2026-06-18T15:00:00.000Z'),
status: 'PUBLISHED',
isHot: false,
displayOrder: 0,
matchName: null,
stage: null,
groupName: null,
league: { logoUrl: null },
homeTeam: { code: 'HME', logoUrl: null },
awayTeam: { code: 'AWY', logoUrl: null },
score: null,
markets: [],
},
]);
const result = await service.listUpcomingPublished('zh-CN');
expect(prisma.match.findMany).toHaveBeenCalledWith(
expect.objectContaining({
where: expect.objectContaining({
status: { in: ['PUBLISHED', 'CLOSED', 'PENDING_SETTLEMENT'] },
isOutright: false,
sportType: 'FOOTBALL',
deletedAt: null,
startTime: { gte: now, lte: end },
}),
orderBy: [{ startTime: 'asc' }, { displayOrder: 'asc' }],
take: 50,
}),
);
expect(result).toHaveLength(1);
expect(result[0]).toEqual(
expect.objectContaining({
id: '10',
startTime: '2026-06-18T15:00:00.000Z',
isHot: false,
}),
);
});
it('respects custom limit and days options', async () => {
prisma.match.findMany.mockResolvedValue([]);
await service.listUpcomingPublished('en-US', { limit: 20, days: 5 });
const now = new Date('2026-06-17T12:00:00.000Z');
const end = new Date(now);
end.setDate(end.getDate() + 5);
expect(prisma.match.findMany).toHaveBeenCalledWith(
expect.objectContaining({
where: expect.objectContaining({
startTime: { gte: now, lte: end },
}),
take: 20,
}),
);
});
});

View File

@@ -1461,6 +1461,42 @@ export class MatchesService {
);
}
/** 未来 N 天内开赛的已发布赛事(按开赛时间升序,不限 isHot */
async listUpcomingPublished(
locale = 'en-US',
options?: { limit?: number; days?: number },
) {
const limit = options?.limit ?? 50;
const days = options?.days ?? 3;
const now = new Date();
const end = new Date(now);
end.setDate(end.getDate() + days);
const matches = await this.prisma.match.findMany({
where: {
status: { in: ['PUBLISHED', 'CLOSED', 'PENDING_SETTLEMENT'] },
isOutright: false,
sportType: 'FOOTBALL',
deletedAt: null,
startTime: { gte: now, lte: end },
league: { isActive: true, deletedAt: null },
},
include: {
league: true,
homeTeam: true,
awayTeam: true,
score: true,
markets: this.playerMarketStatusInclude,
},
orderBy: [{ startTime: 'asc' }, { displayOrder: 'asc' }],
take: limit,
});
return Promise.all(
matches.map((m) => this.enrichMatch(m, locale, { omitMarkets: true })),
);
}
async getMatchDetail(matchId: bigint, locale = 'en-US') {
const match = await this.prisma.match.findFirst({
where: {
@@ -1483,6 +1519,24 @@ export class MatchesService {
return this.enrichMatch(match, locale);
}
async getSelectionsOdds(ids: bigint[]) {
const selections = await this.prisma.marketSelection.findMany({
where: { id: { in: ids } },
include: { market: { include: { match: true } } },
});
return selections.map((sel) => ({
id: sel.id.toString(),
odds: sel.odds.toString(),
oddsVersion: sel.oddsVersion.toString(),
status: sel.status,
marketStatus: sel.market.status,
marketShowOnPlayer: sel.market.showOnPlayer,
matchStatus: sel.market.match.status,
matchId: sel.market.match.id.toString(),
}));
}
async listOutrights(locale = 'en-US') {
try {
await syncWc2026OutrightMarket(this.prisma, { forceCanonical: false });