feat: internationalize API error responses by locale
Add shared error codes with zh/en/ms messages, coded app exceptions, and locale-aware global filter. Frontends send X-Locale so error text matches the active UI language. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -1,12 +1,11 @@
|
||||
import {
|
||||
BadRequestException,
|
||||
Injectable,
|
||||
NotFoundException,
|
||||
} from '@nestjs/common';
|
||||
import { PrismaService } from '../../shared/prisma/prisma.service';
|
||||
import { MarketsService } from '../odds/markets.service';
|
||||
import { WC2026_LEAGUE_CODE, WC2026_OUTRIGHT_TEAMS } from './wc2026-outright-teams';
|
||||
import { syncWc2026OutrightMarket } from './wc2026-outright.sync';
|
||||
import { appBadRequest, appNotFound } from '../../shared/common/app-error';
|
||||
|
||||
const PLACEHOLDER_TEAM_CODE = 'OUT';
|
||||
const OUTRIGHT_MARKET_TYPE = 'OUTRIGHT_WINNER';
|
||||
@@ -200,7 +199,7 @@ export class OutrightService {
|
||||
const league = await this.prisma.league.findUnique({
|
||||
where: { id: leagueId },
|
||||
});
|
||||
if (!league) throw new NotFoundException('League not found');
|
||||
if (!league) throw appNotFound('LEAGUE_NOT_FOUND');
|
||||
const [leagueZh, leagueEn, leagueMs] = await Promise.all([
|
||||
this.getTranslation('LEAGUE', leagueId, 'zh-CN'),
|
||||
this.getTranslation('LEAGUE', leagueId, 'en-US'),
|
||||
@@ -319,7 +318,7 @@ export class OutrightService {
|
||||
const league = await this.prisma.league.findUnique({
|
||||
where: { id: data.leagueId },
|
||||
});
|
||||
if (!league) throw new NotFoundException('League not found');
|
||||
if (!league) throw appNotFound('LEAGUE_NOT_FOUND');
|
||||
|
||||
const placeholder = await this.ensurePlaceholderTeam();
|
||||
const status = data.status ?? 'PUBLISHED';
|
||||
@@ -408,10 +407,10 @@ export class OutrightService {
|
||||
},
|
||||
) {
|
||||
if (!data.teamCode?.trim()) {
|
||||
throw new BadRequestException('Team code required');
|
||||
throw appBadRequest('TEAM_CODE_REQUIRED');
|
||||
}
|
||||
if (data.odds <= 1) {
|
||||
throw new BadRequestException('Odds must be greater than 1');
|
||||
throw appBadRequest('ODDS_MIN');
|
||||
}
|
||||
|
||||
const match = await this.getOutrightMatchOrThrow(matchId);
|
||||
@@ -452,7 +451,7 @@ export class OutrightService {
|
||||
});
|
||||
return this.getForAdmin(matchId);
|
||||
}
|
||||
throw new BadRequestException('Selection already exists for this team code');
|
||||
throw appBadRequest('OUTRIGHT_SELECTION_EXISTS');
|
||||
}
|
||||
|
||||
const maxSort = await this.prisma.marketSelection.aggregate({
|
||||
@@ -485,7 +484,7 @@ export class OutrightService {
|
||||
}>,
|
||||
) {
|
||||
if (!items.length) {
|
||||
throw new BadRequestException('At least one team required');
|
||||
throw appBadRequest('OUTRIGHT_TEAMS_REQUIRED');
|
||||
}
|
||||
let added = 0;
|
||||
let skipped = 0;
|
||||
@@ -521,7 +520,7 @@ export class OutrightService {
|
||||
const sel = await this.prisma.marketSelection.findFirst({
|
||||
where: { id: selectionId, marketId: market.id },
|
||||
});
|
||||
if (!sel) throw new NotFoundException('Selection not found');
|
||||
if (!sel) throw appNotFound('SELECTION_NOT_FOUND');
|
||||
|
||||
const nextCode = data.teamCode?.trim().toUpperCase() || sel.selectionCode;
|
||||
if (nextCode !== sel.selectionCode) {
|
||||
@@ -533,7 +532,7 @@ export class OutrightService {
|
||||
},
|
||||
});
|
||||
if (dup) {
|
||||
throw new BadRequestException('Selection already exists for this team code');
|
||||
throw appBadRequest('OUTRIGHT_SELECTION_EXISTS');
|
||||
}
|
||||
await this.prisma.marketSelection.update({
|
||||
where: { id: selectionId },
|
||||
@@ -583,7 +582,7 @@ export class OutrightService {
|
||||
const sel = await this.prisma.marketSelection.findFirst({
|
||||
where: { id: selectionId, marketId: market.id },
|
||||
});
|
||||
if (!sel) throw new NotFoundException('Selection not found');
|
||||
if (!sel) throw appNotFound('SELECTION_NOT_FOUND');
|
||||
await this.prisma.marketSelection.update({
|
||||
where: { id: selectionId },
|
||||
data: { status: 'CLOSED' },
|
||||
@@ -609,7 +608,7 @@ export class OutrightService {
|
||||
|
||||
for (const u of updates) {
|
||||
if (!allowed.has(u.selectionId)) {
|
||||
throw new BadRequestException('Invalid selection for this outright event');
|
||||
throw appBadRequest('OUTRIGHT_SELECTION_INVALID');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -740,7 +739,7 @@ export class OutrightService {
|
||||
const match = await this.prisma.match.findFirst({
|
||||
where: { id: matchId, isOutright: true, deletedAt: null },
|
||||
});
|
||||
if (!match) throw new NotFoundException('Outright event not found');
|
||||
if (!match) throw appNotFound('OUTRIGHT_EVENT_NOT_FOUND');
|
||||
return match;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user