Compare commits

...

2 Commits

Author SHA1 Message Date
701dfbe2f5 Update passport.client.ts 2026-10-22 14:37:09 +05:00
d1097a5089 Update rounds.service.ts 2026-10-22 14:37:07 +05:00
2 changed files with 28 additions and 8 deletions

View File

@ -1,9 +1,21 @@
import { BadGatewayException, BadRequestException, ConflictException, Injectable, Logger, UnauthorizedException } from '@nestjs/common';
import { BadGatewayException, BadRequestException, ConflictException, HttpException, HttpStatus, Injectable, Logger, UnauthorizedException } from '@nestjs/common';
import { createHash } from 'crypto';
import { config } from '../../config';
// Ответ /api/v1/*: { result } или { errors: [{ code, input_field, description }] }.
type V1Response<T> = { result?: T; errors?: { code: number; input_field?: string | null; description?: string | null }[] };
// description — фраза паспорта на нескольких языках: { ru, en } (App\\Language\\Phrase), иногда строка.
type V1Description = string | { ru?: string; en?: string } | null;
type V1Response<T> = { result?: T; errors?: { code: number; input_field?: string | null; description?: V1Description }[] };
function describe(d: V1Description | undefined): string | null {
if (!d) return null;
if (typeof d === 'string') return d.trim() || null;
return d.ru?.trim() || d.en?.trim() || null;
}
// Коды 1002–1007 — не ошибка человека, а неверная настройка связи с паспортом:
// нет метода, неверная подпись, нет платформы (config/api_v1_errors.php паспорта).
const SETUP_CODES = new Set([1002, 1003, 1004, 1005, 1006, 1007]);
export type PassportUserFull = {
uuid: string;
@ -39,13 +51,15 @@ export type PassportPerson = {
isEnabled: boolean;
};
export class PassportApiError extends Error {
// Ошибка паспорта, которую код не превратил во что-то понятнее (humanize): отдаём 502 с текстом паспорта,
// а не 500 «Internal server error».
export class PassportApiError extends HttpException {
constructor(
readonly code: number,
readonly field: string | null,
message: string,
) {
super(message);
super(message, HttpStatus.BAD_GATEWAY);
}
}
@ -118,8 +132,13 @@ export class PassportClient {
const r = data as V1Response<T>;
if (r?.errors?.length) {
const e = r.errors[0];
this.logger.warn(`Паспорт ${method}: ошибка ${e.code} ${e.input_field ?? ''} ${e.description ?? ''}`);
throw new PassportApiError(e.code, e.input_field ?? null, e.description || `Ошибка паспорта ${e.code}`);
const text = describe(e.description) || `Ошибка паспорта ${e.code}`;
if (SETUP_CODES.has(e.code)) {
this.logger.error(`Паспорт ${method}: ${text} (код ${e.code}) — проверьте PASSPORT_URL, PLATFORM_CODE, PASSPORT_PLATFORM_TOKEN и что метод выложен`);
throw new PassportApiError(e.code, null, `Паспорт Aspans не настроен для «Обходов»: ${text} (код ${e.code})`);
}
this.logger.warn(`Паспорт ${method}: ошибка ${e.code}${e.input_field ? ` [${e.input_field}]` : ''} ${text}`);
throw new PassportApiError(e.code, e.input_field ?? null, text);
}
if (!r || !('result' in r)) throw new BadGatewayException('Паспорт Aspans ответил без результата');
return r.result as T;
@ -224,7 +243,7 @@ export class PassportClient {
}
private humanize(e: unknown) {
if (!(e instanceof PassportApiError)) return e;
if (!(e instanceof PassportApiError) || SETUP_CODES.has(e.code)) return e;
if (e.field === 'email' && /разным/.test(e.message)) {
return new ConflictException('Этот email и этот телефон принадлежат разным людям в Aspans ID');
}

View File

@ -252,7 +252,8 @@ export class RoundsService {
@Cron(CronExpression.EVERY_MINUTE)
async closeExpired() {
if (config.disableCron) return;
const closed: { id: string; site_id: string; status: RoundStatus; organization_id: string; done: number; total: number }[] =
// Для UPDATE драйвер TypeORM (postgres) возвращает не строки, а пару [строки RETURNING, число изменённых].
const [closed]: [{ id: string; site_id: string; status: RoundStatus; organization_id: string; done: number; total: number }[], number] =
await this.db.query(
`UPDATE rounds r SET status = CASE
WHEN r.checkpoints_done = 0 THEN 'missed'