feat(web): типы и клиент API таблицы корректировок (#206)
Типы 1:1 с components.schemas контракта корректировок, клиент API (таблица, сохранение, отзыв, разнесение, экспорт), флаг REACT_APP_CORRECTIONS_MOCK в Config. Ветка фейка пока бросает ошибку (фейк — в следующей задаче). Строка R-1 в design.md. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
59bbf484cf
commit
8eefa3ba02
@ -179,3 +179,4 @@
|
||||
| 28.10.2026 | `expected_version_no = null`, когда действующей корректировки нет (и после отзыва) | иначе после отзыва фронту неоткуда взять номер | — |
|
||||
| 28.10.2026 | Составная статья правил 1 и 2 — в окне первый код, можно выбрать второй | `line_code` — один код; особый расчёт правил 1 и 2 (DOC-274230) — на сервере | при ответе «нужно распределение по обеим» — аналитик добавляет вторую статью руками |
|
||||
| 28.10.2026 | «По пресету» показывает и подходящие нераспределённые строки; «Строка баланса» и «Отклонение» скрывают нераспределённые | пресет — рабочее место по правилу, строки для отнесения нужны рядом | поменять функцию фильтров |
|
||||
| 28.10.2026 | R-1: список правил в плане — ориентир; при расхождении с api/seed/core/elimination_rules_v1.yaml (в плане статья 1230/1510 встречается дважды) номера и статьи берутся из yaml, тесты плана про правила 1, 3, 6, 7 остаются верными | seed — источник истины | поправка mockData и тестов |
|
||||
|
||||
@ -9,4 +9,7 @@ export const Config = {
|
||||
CLIENT_ID: environment.REACT_APP_CLIENT_ID ?? '',
|
||||
NAMESPACE: environment.REACT_APP_NAMESPACE ?? '',
|
||||
APP_NAME: environment.REACT_APP_APP_NAME ?? '',
|
||||
|
||||
/** Таблица корректировок работает на фейке контракта вместо API (до готовности бэкенда AURORA-1540). */
|
||||
CORRECTIONS_MOCK: environment.REACT_APP_CORRECTIONS_MOCK === 'true',
|
||||
};
|
||||
|
||||
13
web/src/services/api/corrections.test.ts
Normal file
13
web/src/services/api/corrections.test.ts
Normal file
@ -0,0 +1,13 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { correctionsPath } from './corrections';
|
||||
|
||||
describe('correctionsPath', () => {
|
||||
it('таблица версии', () => {
|
||||
expect(correctionsPath('v1')).toBe('/consolidations/versions/v1/corrections');
|
||||
});
|
||||
it('агрегат по ключу (rule_id, company_1_id, company_2_id)', () => {
|
||||
expect(correctionsPath('v1', { rule_id: 'r', company_1_id: 'a', company_2_id: 'b' })).toBe(
|
||||
'/consolidations/versions/v1/corrections/aggregates/r/a/b',
|
||||
);
|
||||
});
|
||||
});
|
||||
49
web/src/services/api/corrections.ts
Normal file
49
web/src/services/api/corrections.ts
Normal file
@ -0,0 +1,49 @@
|
||||
import { api, apiFetchBlob } from '../../api/client';
|
||||
import { Config } from '../../conf/config';
|
||||
import type {
|
||||
AggregateKey,
|
||||
AllocationIn,
|
||||
CorrectionAggregateOut,
|
||||
CorrectionIn,
|
||||
CorrectionRevokeIn,
|
||||
CorrectionTableOut,
|
||||
StatementType,
|
||||
} from '../../types/corrections';
|
||||
|
||||
/** Пока фейк не подключён (Task 2), флаг `CORRECTIONS_MOCK` не должен давать молчаливый пустой ответ. */
|
||||
const fakeNotReady = (): never => {
|
||||
throw new Error('фейк в Task 2');
|
||||
};
|
||||
|
||||
const STATEMENT_TYPE: StatementType = 'balance';
|
||||
|
||||
export function correctionsPath(versionId: string, key?: AggregateKey): string {
|
||||
const base = `/consolidations/versions/${versionId}/corrections`;
|
||||
if (!key) return base;
|
||||
return `${base}/aggregates/${key.rule_id}/${key.company_1_id}/${key.company_2_id}`;
|
||||
}
|
||||
|
||||
export function getCorrectionTable(versionId: string): Promise<CorrectionTableOut> {
|
||||
if (Config.CORRECTIONS_MOCK) return fakeNotReady();
|
||||
return api.get<CorrectionTableOut>(correctionsPath(versionId), { statement_type: STATEMENT_TYPE });
|
||||
}
|
||||
|
||||
export function saveCorrection(versionId: string, key: AggregateKey, body: CorrectionIn): Promise<CorrectionAggregateOut> {
|
||||
if (Config.CORRECTIONS_MOCK) return fakeNotReady();
|
||||
return api.put<CorrectionAggregateOut>(correctionsPath(versionId, key), body);
|
||||
}
|
||||
|
||||
export function revokeCorrection(versionId: string, key: AggregateKey, body: CorrectionRevokeIn): Promise<CorrectionAggregateOut> {
|
||||
if (Config.CORRECTIONS_MOCK) return fakeNotReady();
|
||||
return api.post<CorrectionAggregateOut>(`${correctionsPath(versionId, key)}/revoke`, body);
|
||||
}
|
||||
|
||||
export function allocate(versionId: string, body: AllocationIn): Promise<CorrectionTableOut> {
|
||||
if (Config.CORRECTIONS_MOCK) return fakeNotReady();
|
||||
return api.post<CorrectionTableOut>(`${correctionsPath(versionId)}/allocations`, body);
|
||||
}
|
||||
|
||||
export function exportCorrectionTable(versionId: string): Promise<{ blob: Blob; filename: string | null }> {
|
||||
if (Config.CORRECTIONS_MOCK) return fakeNotReady();
|
||||
return apiFetchBlob(`${correctionsPath(versionId)}/export?statement_type=${STATEMENT_TYPE}`);
|
||||
}
|
||||
176
web/src/types/corrections.ts
Normal file
176
web/src/types/corrections.ts
Normal file
@ -0,0 +1,176 @@
|
||||
// Типы таблицы корректировок 1:1 с components.schemas контракта (docs/dev/api-contract-corrections.openapi.yaml).
|
||||
// Деньги и проценты — строки с двумя знаками, в рублях; перевод в тыс. руб. и округление — на фронте.
|
||||
|
||||
export type StatementType = 'balance' | 'income';
|
||||
|
||||
export type Side = 'D' | 'K';
|
||||
|
||||
/** Сумма в рублях, два знака (`"1000000.00"`). */
|
||||
export type Money = string;
|
||||
|
||||
/** Неотрицательная сумма для ввода, не более двух знаков. */
|
||||
export type MoneyIn = string;
|
||||
|
||||
/** Процент с двумя знаками; `null` — не определён. */
|
||||
export type Percent = string | null;
|
||||
|
||||
export interface Ref {
|
||||
id: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
export type ReadOnlyReason = 'VERSION_NOT_EDITABLE' | 'LOCK_REQUIRED' | 'LOCKED_BY_OTHER';
|
||||
|
||||
export interface CorrectionRuleOut {
|
||||
id: string;
|
||||
rule_no: number;
|
||||
/** Код статьи; составная — коды через «/» (`"1450/1550"`). */
|
||||
article_1_line: string;
|
||||
article_2_line: string;
|
||||
side_1: Side;
|
||||
measure: 'closing' | 'turnover';
|
||||
company_1_accounts: string[];
|
||||
company_2_accounts: string[];
|
||||
company_1_excluded: string[];
|
||||
company_2_excluded: string[];
|
||||
}
|
||||
|
||||
export interface CorrectionEntryOut {
|
||||
entry_ids: string[];
|
||||
legal_entity_id: string;
|
||||
counterparty_id: string;
|
||||
account_code: string;
|
||||
side: Side;
|
||||
/** Сальдо (оборот) строки по этой стороне. */
|
||||
amount: Money;
|
||||
decision: 'AUTO' | 'ASSIGN';
|
||||
/** «Не включать в КФО»; в сумму стороны не входит. */
|
||||
excluded: boolean;
|
||||
candidate_rule_ids: string[];
|
||||
}
|
||||
|
||||
export interface AggregateSideOut {
|
||||
article_line: string;
|
||||
accounts: string[];
|
||||
amount: Money;
|
||||
entries: CorrectionEntryOut[];
|
||||
}
|
||||
|
||||
export interface CorrectionLineOut {
|
||||
line_code: string;
|
||||
amount: Money;
|
||||
}
|
||||
|
||||
export interface CorrectionBalanceOut {
|
||||
debit_total: Money;
|
||||
credit_total: Money;
|
||||
difference: Money;
|
||||
difference_percent: Percent;
|
||||
/** Сумма стороны 1 минус debit_total. */
|
||||
remainder_1: Money;
|
||||
/** Сумма стороны 2 минус credit_total. */
|
||||
remainder_2: Money;
|
||||
}
|
||||
|
||||
export interface CorrectionOut {
|
||||
status: 'DRAFT' | 'APPLIED';
|
||||
version_no: number;
|
||||
/** Статьи стороны 1, первая — основная. */
|
||||
debit: CorrectionLineOut[];
|
||||
/** Статьи стороны 2, первая — основная. */
|
||||
credit: CorrectionLineOut[];
|
||||
comment: string;
|
||||
reason_code: string | null;
|
||||
balance: CorrectionBalanceOut;
|
||||
updated_by: Ref | null;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
export type AggregateStatus = 'AUTO' | 'DISCREPANCY' | 'ONE_SIDED' | 'MANUAL';
|
||||
|
||||
export interface CorrectionAggregateOut {
|
||||
rule_id: string;
|
||||
rule_no: number;
|
||||
company_1: Ref;
|
||||
company_2: Ref;
|
||||
side_1: AggregateSideOut;
|
||||
side_2: AggregateSideOut;
|
||||
deviation_amount: Money;
|
||||
deviation_percent: Percent;
|
||||
/** deviation_percent ≥ 5, или одна сторона пуста при ненулевой другой. */
|
||||
exceeds_threshold: boolean;
|
||||
status: AggregateStatus;
|
||||
eliminated_amount: Money;
|
||||
/** Состав или суммы изменились после применённой корректировки. */
|
||||
composition_changed: boolean;
|
||||
correction: CorrectionOut | null;
|
||||
}
|
||||
|
||||
export interface UnallocatedEntryOut {
|
||||
entry_ids: string[];
|
||||
owner: Ref;
|
||||
counterparty: Ref;
|
||||
account_code: string;
|
||||
side: Side;
|
||||
amount: Money;
|
||||
candidate_rule_ids: string[];
|
||||
reason: 'MULTIPLE_RULES' | 'UNASSIGNED';
|
||||
excluded: boolean;
|
||||
}
|
||||
|
||||
export interface CorrectionTotalsOut {
|
||||
amount_1: Money;
|
||||
amount_2: Money;
|
||||
deviation_amount: Money;
|
||||
correction_debit: Money;
|
||||
correction_credit: Money;
|
||||
}
|
||||
|
||||
export interface CorrectionTableOut {
|
||||
version_id: string;
|
||||
statement_type: StatementType;
|
||||
rules_version_no: number;
|
||||
/** Может ли текущий пользователь править таблицу. */
|
||||
editable: boolean;
|
||||
read_only_reason: ReadOnlyReason | null;
|
||||
rules: CorrectionRuleOut[];
|
||||
aggregates: CorrectionAggregateOut[];
|
||||
unallocated: UnallocatedEntryOut[];
|
||||
totals: CorrectionTotalsOut;
|
||||
}
|
||||
|
||||
export interface CorrectionLineIn {
|
||||
line_code: string;
|
||||
amount: MoneyIn;
|
||||
}
|
||||
|
||||
export interface CorrectionIn {
|
||||
debit: CorrectionLineIn[];
|
||||
credit: CorrectionLineIn[];
|
||||
comment: string;
|
||||
reason_code?: string | null;
|
||||
status?: 'APPLIED' | 'DRAFT';
|
||||
/** Редакция действующей корректировки, которую видел аналитик; `null` — действующей нет. */
|
||||
expected_version_no: number | null;
|
||||
}
|
||||
|
||||
export interface CorrectionRevokeIn {
|
||||
comment: string;
|
||||
}
|
||||
|
||||
export type AllocationAction = 'ASSIGN' | 'UNASSIGN' | 'EXCLUDE' | 'INCLUDE' | 'RESET';
|
||||
|
||||
export interface AllocationIn {
|
||||
statement_type: StatementType;
|
||||
entry_ids: string[];
|
||||
action: AllocationAction;
|
||||
/** Обязателен для ASSIGN. */
|
||||
rule_id?: string | null;
|
||||
}
|
||||
|
||||
/** Ключ агрегата в пути запроса: `(rule_id, company_1_id, company_2_id)`. */
|
||||
export interface AggregateKey {
|
||||
rule_id: string;
|
||||
company_1_id: string;
|
||||
company_2_id: string;
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user