import { ApiError } from '../../api/client'; import type { CandidateOut, CandidateScope, ConsolidatedRowOut, MapCounterpartyOut } from '../../types/api'; /** `scope=all` с более коротким `q` сервер отклоняет (422 `QUERY_TOO_SHORT`). */ export const ALL_SCOPE_MIN_QUERY = 3; const MAX_QUERY = 500; /** «Указать» — у строки без сопоставленного ЮЛ (как баннер «Осталось N» = `unmapped_total`). */ export const needsMapping = (row: ConsolidatedRowOut): boolean => row.counterparty_id === null; /** Склеенная из кварталов строка несёт несколько `entry_ids` (от раннего к позднему); сопоставляется последняя. */ export const mapEntryId = (row: ConsolidatedRowOut): string => // biome-ignore lint/nursery/useAtIndex: lib tsconfig — ES2020, Array.prototype.at недоступен row.entry_ids[row.entry_ids.length - 1] ?? ''; /** Query `GET …/counterparty-candidates`; `null` — запрос не отправлять (`scope=all` без 3 символов поиска). */ export function buildCandidatesQuery(entryId: string, scope: CandidateScope, q: string): string | null { const query = q.trim().slice(0, MAX_QUERY); if (scope === 'all' && query.length < ALL_SCOPE_MIN_QUERY) return null; const params = new URLSearchParams({ scope, entry_id: entryId }); if (query) params.set('q', query); return `?${params.toString()}`; } export type CandidateRow = | { kind: 'section'; key: string; label: string } | { kind: 'candidate'; key: string; candidate: CandidateOut; recommended: boolean }; /** Строки таблицы модалки: заголовок «Рекомендуемые» и рекомендуемые кандидаты, затем остальные (макет [32-osv-map-counterparty]). */ export function candidateRows(candidates: CandidateOut[]): CandidateRow[] { const toRow = (c: CandidateOut): CandidateRow => ({ kind: 'candidate', key: c.id, candidate: c, recommended: c.recommended }); const recommended = candidates.filter((c) => c.recommended).map(toRow); const rest = candidates.filter((c) => !c.recommended).map(toRow); const section: CandidateRow[] = recommended.length > 0 ? [{ kind: 'section', key: 'section:recommended', label: 'Рекомендуемые' }] : []; return [...section, ...recommended, ...rest]; } export type MapErrorKind = 'ALIAS_TAKEN' | 'LOCK_REQUIRED' | 'LOCKED_BY_OTHER' | 'VERSION_NOT_EDITABLE' | 'OTHER'; const KNOWN_CONFLICTS: readonly MapErrorKind[] = ['ALIAS_TAKEN', 'LOCK_REQUIRED', 'LOCKED_BY_OTHER', 'VERSION_NOT_EDITABLE']; /** Вид ошибки `POST …/osv/entries/{entry_id}/counterparty`, на который интерфейс отвечает по-разному. */ export function mapErrorKind(e: unknown): MapErrorKind { if (e instanceof ApiError && e.status === 409) { const kind = KNOWN_CONFLICTS.find((k) => k === e.code); if (kind) return kind; } return 'OTHER'; } export type AliasOwner = { legal_entity_id: string; name: string }; const isRecord = (v: unknown): v is Record => typeof v === 'object' && v !== null && !Array.isArray(v); /** Владельцы алиаса из `409 ALIAS_TAKEN` (`detail.owners[]`); непохожие элементы отбрасываются. */ export function aliasOwners(e: unknown): AliasOwner[] { if (!(e instanceof ApiError) || !isRecord(e.body) || !isRecord(e.body.detail)) return []; const owners = e.body.detail.owners; if (!Array.isArray(owners)) return []; return owners.filter((o): o is AliasOwner => isRecord(o) && typeof o.legal_entity_id === 'string' && typeof o.name === 'string'); } /** Имя держателя блокировки: есть только в 409 `LOCKED_BY_OTHER` от `POST …/lock`; у правок версии — нет (берётся из карточки). */ export function lockedByName(e: unknown): string | null { if (!(e instanceof ApiError) || !isRecord(e.body) || !isRecord(e.body.detail)) return null; const name = e.body.detail.locked_by_name; return typeof name === 'string' && name ? name : null; } /** Текст после успешного «Связать»: с кем связано, сколько строк сопоставлено автоприменением, что с алиасом. */ export function mapResultText(out: MapCounterpartyOut, entityName: string): string { const parts = [`Связано с ${entityName}.`]; if (out.propagated > 0) parts.push(`Также сопоставлено строк ОСВ: ${out.propagated}.`); if (out.alias_moved_from) parts.push('Алиас перенесён от другой организации.'); else if (out.alias_created) parts.push('Имя из ОСВ сохранено как алиас организации.'); return parts.join(' '); }