feat(organizations): migrate source storage to polymorphic records

This commit is contained in:
2026-05-19 10:23:53 +02:00
parent 19a7d5a91c
commit 4ca2fa25d5
44 changed files with 7129 additions and 1551 deletions

View File

@@ -0,0 +1,257 @@
import assert from "node:assert/strict";
import test from "node:test";
import {
MostovikOrganizationsClient,
type ApiErrorPayload,
type FetchLike,
type GenericSourcePayload,
type Organization,
type OrganizationListResponse,
} from "../src/index.js";
function jsonResponse(payload: unknown, init: ResponseInit = {}): Response {
return new Response(JSON.stringify(payload), {
status: init.status ?? 200,
headers: {
"content-type": "application/json",
...init.headers,
},
});
}
test("listOrganizations serializes all supported organization filters", async () => {
const calls: RequestInfo[] = [];
const fetchImpl: FetchLike = async (input) => {
calls.push(input);
const payload: OrganizationListResponse = {
success: true,
data: [],
errors: null,
meta: {
pagination: {
page: 2,
page_size: 50,
total_count: 0,
total_pages: 0,
has_next: false,
has_previous: true,
},
},
};
return jsonResponse(payload);
};
const client = new MostovikOrganizationsClient({
baseUrl: "https://api.example.test/",
fetch: fetchImpl,
headers: { Authorization: "Bearer static-token" },
});
const response = await client.listOrganizations({
page: 2,
page_size: 50,
search: "мост",
ordering: "-name",
name: "Северный",
inn: "7711111111",
kpp: "771101001",
ogrn: "1027700132111",
ogrip: "304500116000157",
registry: "8e7a3cb8-6fb2-43a8-b847-62d84ea9f34f",
registry_name: "Росатом",
has_registry: false,
has_industrial: true,
has_industrial_products: false,
has_manufactures: true,
has_inspections: false,
has_procurements: true,
has_procurements_44fz: false,
has_procurements_223fz: true,
has_contracts: false,
has_unfair_suppliers: true,
has_fas_goz: false,
has_arbitration: true,
has_fedresurs_bankruptcy: false,
has_fstec: true,
has_vacancies: true,
has_trudvsem: false,
has_fns_reports: false,
data: ["industrial", "fns_reports"],
exclude_data_sources: "vacancies",
});
assert.equal(response.success, true);
assert.equal(calls.length, 1);
const url = new URL(String(calls[0]));
assert.equal(url.origin, "https://api.example.test");
assert.equal(url.pathname, "/api/v2/organizations/");
assert.equal(url.searchParams.get("page"), "2");
assert.equal(url.searchParams.get("page_size"), "50");
assert.equal(url.searchParams.get("ordering"), "-name");
assert.equal(url.searchParams.get("has_registry"), "false");
assert.deepEqual(url.searchParams.getAll("data"), ["industrial", "fns_reports"]);
assert.equal(url.searchParams.get("exclude_data_sources"), "vacancies");
assert.equal(url.searchParams.get("has_vacancies"), "true");
assert.equal(url.searchParams.get("has_trudvsem"), "false");
});
test("getOrganization requests detail endpoint and returns typed source data", async () => {
const calls: RequestInfo[] = [];
const uid = "5dc142b3-dcf4-4b90-807a-a80e883dd05c";
const payload: Organization = {
uid,
name: "ООО \"Данные\"",
normalized_name: "ООО \"Данные\"",
inn: "7777777777",
kpp: "777701001",
ogrn: "1027700132777",
ogrip: "",
registries: [
{
id: "0b85bf08-c6d9-4c07-98c3-a057630e3a35",
name: "Росатом ГОЗ",
},
],
data_sources: [
{
source: "industrial",
count: 1,
},
{
source: "fns_reports",
count: 1,
},
],
data: {
industrial: [
{
id: 1,
load_batch: 10,
issue_date: "01.01.2025",
issue_date_normalized: "2025-01-01",
certificate_number: "CERT-1",
expiry_date: "",
expiry_date_normalized: null,
certificate_file_url: "https://example.test/cert.pdf",
organisation_name: "ООО \"Данные\"",
inn: "7777777777",
ogrn: "1027700132777",
registry_organization: null,
created_at: "2026-05-01T10:00:00Z",
updated_at: "2026-05-01T10:00:00Z",
},
],
fns_reports: [
{
id: 2,
external_id: "fin-1",
ogrn: "1027700132777",
registry_organization: null,
file_name: "fin.xlsx",
file_hash: "a".repeat(64),
load_batch: 11,
status: "success",
source: "api",
error_message: "",
created_at: "2026-05-01T10:00:00Z",
updated_at: "2026-05-01T10:00:00Z",
lines_count: 1,
lines: {
"2024": {
active: {
"1100": {
form_code: "1",
name: "Внеоборотные активы",
period_start: 100,
period_end: 200,
},
},
},
},
},
],
},
};
const fetchImpl: FetchLike = async (input) => {
calls.push(input);
return jsonResponse(payload);
};
const client = new MostovikOrganizationsClient({
baseUrl: "https://api.example.test",
fetch: fetchImpl,
});
const organization = await client.getOrganization(uid, {
data_sources: ["industrial", "fns_reports"],
});
assert.equal(organization.uid, uid);
assert.equal(organization.data.industrial?.[0]?.certificate_number, "CERT-1");
assert.equal(
organization.data.fns_reports?.[0]?.lines["2024"]?.active?.["1100"]?.period_end,
200,
);
const url = new URL(String(calls[0]));
assert.equal(url.pathname, `/api/v2/organizations/${uid}/`);
assert.deepEqual(url.searchParams.getAll("data_sources"), [
"industrial",
"fns_reports",
]);
});
test("throws ApiClientError with status and payload for non-2xx responses", async () => {
const errorPayload: ApiErrorPayload = {
success: false,
data: null,
errors: [
{
code: "validation_error",
message: "Validation failed",
details: {
fields: {
data: ["Unknown data source(s): unknown"],
},
},
},
],
meta: {
request_id: "test-request-id",
},
};
const fetchImpl: FetchLike = async () =>
jsonResponse(errorPayload, { status: 400 });
const client = new MostovikOrganizationsClient({
baseUrl: "https://api.example.test",
fetch: fetchImpl,
});
await assert.rejects(
() => client.getOrganization("uid", { data: "industrial" }),
(error: unknown) => {
assert.equal(error instanceof Error, true);
assert.equal((error as { name?: string }).name, "ApiClientError");
assert.equal((error as { status?: number }).status, 400);
assert.deepEqual((error as { payload?: ApiErrorPayload }).payload, errorPayload);
return true;
},
);
});
test("generic source payload is represented as a typed JSON object", () => {
const payload: GenericSourcePayload = {
provider: "checko",
target: {
inn: "7711111199",
ogrn: "1027700132199",
},
suppliers: [
{
inn: "7722222299",
amount: 1000,
},
],
};
assert.equal(payload.provider, "checko");
});