arm-core/web/src/utils/goodwill.ts
Andrey Epifancev 6be3936d9f Гудвилл: снятая галочка с неверной долей не блокирует сохранение
Финальное ревью I-1: у неотмеченной строки поле доли скрыто — effectiveRow отправляет
сохранённую долю вместо неверного ввода; подсказка у «Сохранить» называет строки с ошибкой
и предупреждает о скрытых фильтром. design.md goodwill-ui: R-4…R-6.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-06 18:50:27 +04:00

96 lines
4.7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/** Чистые функции вкладки «Гудвилл» (DOC-259025 §4–§6): ввод доли, предпросмотр P/G, фильтр знака, изменённые строки. */
import type { GoodwillEdit, GoodwillRow, GoodwillRowKeyIn, GoodwillSign } from '../types/goodwill/goodwill';
import { fromCents, toCents } from './money';
const SHARE = /^\d{1,3}(?:\.\d{1,2})?$/;
export function rowKey(row: Pick<GoodwillRow, 'head_legal_entity' | 'subsidiary_legal_entity'>): string {
return `${row.head_legal_entity.id}:${row.subsidiary_legal_entity.id}`;
}
/** Доля 0–100, не больше двух знаков, запятая или точка; пусто — null. Лишние знаки — ошибка, не обрезка. */
export function parseShare(input: string): { ok: true; value: string | null } | { ok: false; error: string } {
const value = input.trim().replace(',', '.');
if (value === '') return { ok: true, value: null };
if (!SHARE.test(value) || shareHundredths(value) > 10000n) {
return { ok: false, error: 'Число от 0 до 100, не больше двух знаков после запятой' };
}
return { ok: true, value };
}
/** Доля строкой («80.5») → сотые доли процента (8050n). */
function shareHundredths(value: string): bigint {
const [int, frac = ''] = value.split('.');
return BigInt(int) * 100n + BigInt(frac.padEnd(2, '0'));
}
/** Предварительные P = round(УК × d / 100, 2) и G = ФВ − P в копейках, половина вверх; как на бэке. */
export function previewAmounts(
row: GoodwillRow,
share: string | null,
include: boolean,
): { participation: string | null; goodwill: string | null } {
const fv = row.financial_investment_amount;
const capital = row.charter_capital_amount;
if (!row.has_investments || !include || share === null || fv === null || capital === null) {
return { participation: null, goodwill: null };
}
const parsed = parseShare(share);
if (!parsed.ok || parsed.value === null) return { participation: null, goodwill: null };
const product = toCents(capital) * shareHundredths(parsed.value);
const divisor = 10000n;
const negative = product < 0n;
const abs = negative ? -product : product;
const rounded = (abs + divisor / 2n) / divisor;
const p = negative ? -rounded : rounded;
return { participation: fromCents(p), goodwill: fromCents(toCents(fv) - p) };
}
/** Фильтр «Гудвилл»: нерассчитанный (null) — только во «Все значения». */
export function matchesSign(goodwill: string | null, sign: GoodwillSign): boolean {
if (sign === 'all') return true;
if (goodwill === null) return false;
const cents = toCents(goodwill);
if (sign === 'positive') return cents > 0n;
if (sign === 'negative') return cents < 0n;
return cents === 0n;
}
/** Значения строки с учётом правки: что уйдёт в пакет. */
export function effectiveRow(row: GoodwillRow, edit: GoodwillEdit | undefined): GoodwillRowKeyIn {
const include = edit?.include_in_kfo ?? row.include_in_kfo;
const share = edit && 'ownership_share' in edit ? (edit.ownership_share ?? null) : row.ownership_share;
const parsed = share === null ? null : parseShare(share);
const comment = edit && 'comment' in edit ? (edit.comment ?? null) : row.comment;
let ownership = parsed?.ok ? parsed.value : share;
// у неотмеченной строки поле доли скрыто: неверный ввод не исправить, поэтому уходит сохранённая доля
if (!include && parsed && !parsed.ok) ownership = row.ownership_share;
return {
head_legal_entity_id: row.head_legal_entity.id,
subsidiary_legal_entity_id: row.subsidiary_legal_entity.id,
ownership_share: ownership,
include_in_kfo: include,
comment: comment?.trim() ? comment : null,
};
}
function sameShare(a: string | null, b: string | null): boolean {
if (a === null || b === null) return a === b;
if (!SHARE.test(a) || !SHARE.test(b)) return a === b;
return shareHundredths(a) === shareHundredths(b);
}
/** Строки пакета — только те, где правка отличается от сохранённого. */
export function changedRows(rows: GoodwillRow[], edits: Record<string, GoodwillEdit>): GoodwillRowKeyIn[] {
return rows.flatMap((row) => {
const edit = edits[rowKey(row)];
if (!edit) return [];
const next = effectiveRow(row, edit);
const same =
next.include_in_kfo === row.include_in_kfo &&
sameShare(next.ownership_share, row.ownership_share) &&
(next.comment?.trim() ?? null) === (row.comment?.trim() || null);
return same ? [] : [next];
});
}