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 | `expected_version_no = null`, когда действующей корректировки нет (и после отзыва) | иначе после отзыва фронту неоткуда взять номер | — |
|
||||||
| 28.10.2026 | Составная статья правил 1 и 2 — в окне первый код, можно выбрать второй | `line_code` — один код; особый расчёт правил 1 и 2 (DOC-274230) — на сервере | при ответе «нужно распределение по обеим» — аналитик добавляет вторую статью руками |
|
| 28.10.2026 | Составная статья правил 1 и 2 — в окне первый код, можно выбрать второй | `line_code` — один код; особый расчёт правил 1 и 2 (DOC-274230) — на сервере | при ответе «нужно распределение по обеим» — аналитик добавляет вторую статью руками |
|
||||||
| 28.10.2026 | «По пресету» показывает и подходящие нераспределённые строки; «Строка баланса» и «Отклонение» скрывают нераспределённые | пресет — рабочее место по правилу, строки для отнесения нужны рядом | поменять функцию фильтров |
|
| 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 ?? '',
|
CLIENT_ID: environment.REACT_APP_CLIENT_ID ?? '',
|
||||||
NAMESPACE: environment.REACT_APP_NAMESPACE ?? '',
|
NAMESPACE: environment.REACT_APP_NAMESPACE ?? '',
|
||||||
APP_NAME: environment.REACT_APP_APP_NAME ?? '',
|
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