ДОМЕНЫ - ВОПРОСЫ - ОТКРЫТЫЕ ДОГОВОРА - Усилить business-view exact открытых договоров: разрез по типам остатков и quality gates

This commit is contained in:
2026-04-13 18:15:06 +03:00
parent 3e588ede81
commit ef4222d159
14 changed files with 984 additions and 49 deletions
@@ -758,6 +758,40 @@ interface CounterpartyRiskAggregate {
lastPeriod: string | null;
}
interface OpenContractRiskAggregate {
contract: string;
totalAmount: number;
operations: number;
firstPeriod: string | null;
lastPeriod: string | null;
counterparties: string[];
sourceRefs: string[];
category: "commercial" | "financial" | "uncertain";
qualityFlags: string[];
}
type OpenContractSettlementKind =
| "receivable"
| "payable"
| "advance_issued"
| "advance_received"
| "other_receivable"
| "other_payable";
interface OpenContractConfirmedAggregate {
contract: string;
counterparty: string | null;
confirmedAmount: number;
operations: number;
firstPeriod: string | null;
lastPeriod: string | null;
category: "commercial" | "financial" | "uncertain";
settlementKind: OpenContractSettlementKind;
accounts: string[];
sourceRefs: string[];
qualityFlags: string[];
}
type PayablesLiabilityCategory = "supplier_or_contractor" | "bank_or_credit" | "tax_or_state" | "other";
interface PayablesCounterpartyRiskAggregate extends CounterpartyRiskAggregate {
@@ -1553,6 +1587,380 @@ export function contractCandidatesFromRows(rows: ComposeStageRow[]): string[] {
return uniqueStrings(candidates);
}
function isFinancialContractLike(value: string): boolean {
return /(?:кредит|кред\.?|loan|overdraft|овердрафт|лизинг|leasing|займ|guarantee|гарант|банк|bank)/iu.test(value);
}
function hasStrongContractIdentitySignal(value: string): boolean {
return /(?:договор|contract|дог\.|№|\d{1,4}[\\/.-]\d{1,4}|\d{1,4}\sот\s\d{2}\.\d{2}\.\d{2,4}|[A-ZА-Я]{1,6}-\d+)/iu.test(
value
);
}
function isLikelyOrganizationName(value: string): boolean {
return /(?:(?:^|[\s"'«»„“()\\\/])(?:ооо|ао|пао|зао|оао|ип|гку)(?=$|[\s"'«»„“()\\\/.,;:]))|комитет|департамент|министерств|служб|управлени|казенн|администрац|bank|банк/iu.test(
value
);
}
function isContractLikeCounterparty(value: string): boolean {
return /(?:договор|дог[-.\s]?р|contract|кредитн|loan|овердрафт|лизинг|\b№\b)/iu.test(value);
}
function isLowQualityContractIdentity(contract: string, counterparty: string | null): boolean {
const normalizedContract = normalizeEntityToken(contract);
if (!normalizedContract || normalizedContract.length < 3) {
return true;
}
if (/^(?:0|<пусто>|пустая ссылка)$/iu.test(contract.trim())) {
return true;
}
if (counterparty && normalizeEntityToken(counterparty) === normalizedContract) {
return true;
}
if (!hasStrongContractIdentitySignal(contract) && isLikelyOrganizationName(contract)) {
return true;
}
if (!hasStrongContractIdentitySignal(contract) && /^[A-ZА-Я]{2,6}$/u.test(contract.trim())) {
return true;
}
return false;
}
function isLowQualityCounterpartyForContract(counterparty: string | null, contract: string): boolean {
if (!counterparty) {
return true;
}
const normalizedCounterparty = normalizeEntityToken(counterparty);
const normalizedContract = normalizeEntityToken(contract);
if (!normalizedCounterparty) {
return true;
}
if (normalizedCounterparty === normalizedContract) {
return true;
}
if (isContractLikeCounterparty(counterparty)) {
return true;
}
return normalizedCounterparty.length < 3;
}
function normalizeDisplayAccountToken(value: string | null): string | null {
const normalized = String(value ?? "").trim();
if (!normalized || /^(?:0|<пусто>|пустая ссылка|-)$/iu.test(normalized)) {
return null;
}
return normalized;
}
function classifyOpenContractCategory(
contract: string,
counterparties: string[],
qualityFlags: string[]
): "commercial" | "financial" | "uncertain" {
if (isFinancialContractLike(contract)) {
return "financial";
}
if (counterparties.some((item) => isFinancialContractLike(item))) {
return "financial";
}
if (
qualityFlags.includes("counterparty_not_reliably_resolved") ||
qualityFlags.includes("contract_identity_not_reliable") ||
qualityFlags.includes("contract_identity_looks_like_counterparty") ||
qualityFlags.includes("multiple_counterparties_for_contract")
) {
return "uncertain";
}
if (counterparties.length === 0) {
return "uncertain";
}
return "commercial";
}
function classifyOpenContractSettlementKind(row: ComposeStageRow): OpenContractSettlementKind | null {
const dt = extractAccountSectionCode(row.account_dt);
const kt = extractAccountSectionCode(row.account_kt);
if (dt === "62") {
return "receivable";
}
if (kt === "60") {
return "payable";
}
if (dt === "60") {
return "advance_issued";
}
if (kt === "62") {
return "advance_received";
}
if (dt === "76") {
return "other_receivable";
}
if (kt === "76") {
return "other_payable";
}
return null;
}
function openContractSettlementKindLabel(kind: OpenContractSettlementKind): string {
if (kind === "receivable") {
return "дебиторская задолженность";
}
if (kind === "payable") {
return "кредиторская задолженность";
}
if (kind === "advance_issued") {
return "аванс выданный";
}
if (kind === "advance_received") {
return "аванс полученный";
}
if (kind === "other_receivable") {
return "прочий дебетовый остаток";
}
return "прочий кредитовый остаток";
}
function summarizeOpenContractSpecialReason(item: { category: "commercial" | "financial" | "uncertain"; qualityFlags: string[] }): string {
if (item.category === "financial") {
return "похоже на финансовый договор (кредит/банк)";
}
if (item.qualityFlags.includes("contract_identity_looks_like_counterparty")) {
return "в поле договора похоже попал контрагент или чужая аналитика";
}
if (item.qualityFlags.includes("contract_identity_not_reliable")) {
return "договор не похож на устойчивый договорный реквизит";
}
if (item.qualityFlags.includes("multiple_counterparties_for_contract")) {
return "по одному договору найдено несколько контрагентов";
}
if (item.qualityFlags.includes("counterparty_not_reliably_resolved")) {
return "не удалось надежно определить контрагента";
}
return "требуется ручная проверка карточки договора";
}
function buildOpenContractConfirmedBalanceAggregate(
rows: ComposeStageRow[],
asOfDate: string
): OpenContractConfirmedAggregate[] {
const byContract = new Map<
string,
{
contract: string;
counterparty: string | null;
confirmedAmount: number;
operations: number;
firstPeriod: string | null;
lastPeriod: string | null;
counterparties: Set<string>;
settlementKind: OpenContractSettlementKind;
accounts: Set<string>;
sourceRefs: Set<string>;
qualityFlags: Set<string>;
}
>();
const asOfTimestamp = toUtcDayTimestamp(asOfDate);
for (const row of rows) {
const rowTimestamp = toUtcDayTimestamp(row.period);
if (asOfTimestamp !== null && rowTimestamp !== null && rowTimestamp > asOfTimestamp) {
continue;
}
const contract = extractContractName(row);
if (!contract) {
continue;
}
const amount = row.amount;
if (typeof amount !== "number" || !Number.isFinite(amount)) {
continue;
}
const settlementKind = classifyOpenContractSettlementKind(row);
if (!settlementKind) {
continue;
}
const counterpartyCandidate = extractCounterpartyName(row);
const counterparty = isLowQualityCounterpartyForContract(counterpartyCandidate, contract) ? null : counterpartyCandidate;
const sourceRefs = extractPayablesSourceRefs(row, counterparty ?? contract, contract);
const accountToken = normalizeDisplayAccountToken(row.account_dt) ?? normalizeDisplayAccountToken(row.account_kt);
const absAmount = Math.abs(amount);
const contractKey = normalizeEntityToken(contract);
const counterpartyKey = counterparty ? normalizeEntityToken(counterparty) : "__unknown_counterparty__";
const aggregateKey = `${contractKey}::${counterpartyKey}::${settlementKind}`;
const current = byContract.get(aggregateKey);
if (!current) {
const qualityFlags = new Set<string>();
if (!counterparty) {
qualityFlags.add("counterparty_not_reliably_resolved");
}
if (isLowQualityContractIdentity(contract, counterparty)) {
qualityFlags.add("contract_identity_not_reliable");
}
if (counterparty && normalizeEntityToken(counterparty) === normalizeEntityToken(contract)) {
qualityFlags.add("contract_identity_looks_like_counterparty");
}
const counterparties = new Set<string>();
if (counterparty) {
counterparties.add(counterparty);
}
const accounts = new Set<string>();
if (accountToken) {
accounts.add(accountToken);
}
byContract.set(aggregateKey, {
contract,
counterparty,
confirmedAmount: absAmount,
operations: 1,
firstPeriod: row.period,
lastPeriod: row.period,
counterparties,
settlementKind,
accounts,
sourceRefs: new Set(sourceRefs),
qualityFlags
});
continue;
}
current.confirmedAmount += absAmount;
current.operations += 1;
if ((row.period ?? "") < (current.firstPeriod ?? "")) {
current.firstPeriod = row.period;
}
if ((row.period ?? "") > (current.lastPeriod ?? "")) {
current.lastPeriod = row.period;
}
if (counterparty) {
current.counterparty = current.counterparty ?? counterparty;
current.counterparties.add(counterparty);
} else {
current.qualityFlags.add("counterparty_not_reliably_resolved");
}
if (accountToken) {
current.accounts.add(accountToken);
}
for (const ref of sourceRefs) {
current.sourceRefs.add(ref);
}
}
return Array.from(byContract.values())
.map((item) => {
const counterparties = Array.from(item.counterparties);
if (counterparties.length > 1) {
item.qualityFlags.add("multiple_counterparties_for_contract");
}
return {
contract: item.contract,
counterparty: item.counterparty,
confirmedAmount: item.confirmedAmount,
operations: item.operations,
firstPeriod: item.firstPeriod,
lastPeriod: item.lastPeriod,
category: classifyOpenContractCategory(item.contract, counterparties, Array.from(item.qualityFlags)),
settlementKind: item.settlementKind,
accounts: Array.from(item.accounts).slice(0, 3),
sourceRefs: Array.from(item.sourceRefs).slice(0, 3),
qualityFlags: Array.from(item.qualityFlags)
};
})
.filter((item) => item.confirmedAmount > 0.005)
.sort((left, right) => {
if (right.confirmedAmount !== left.confirmedAmount) {
return right.confirmedAmount - left.confirmedAmount;
}
if (right.operations !== left.operations) {
return right.operations - left.operations;
}
return left.contract.localeCompare(right.contract);
});
}
function buildOpenContractRiskAggregate(rows: ComposeStageRow[]): OpenContractRiskAggregate[] {
const byContract = new Map<
string,
{
contract: string;
totalAmount: number;
operations: number;
firstPeriod: string | null;
lastPeriod: string | null;
counterparties: Set<string>;
sourceRefs: Set<string>;
qualityFlags: Set<string>;
}
>();
for (const row of rows) {
const contract = extractContractName(row);
if (!contract) {
continue;
}
const amountRaw = row.amount ?? 0;
const amount = Number.isFinite(amountRaw) ? Math.abs(amountRaw) : 0;
const current = byContract.get(contract);
const counterpartyCandidate = extractCounterpartyName(row);
const counterparty = isLowQualityCounterpartyForContract(counterpartyCandidate, contract) ? null : counterpartyCandidate;
const sourceRefs = extractPayablesSourceRefs(row, counterparty ?? contract, contract);
if (!current) {
const qualityFlags = new Set<string>();
if (!counterparty) {
qualityFlags.add("counterparty_not_reliably_resolved");
}
byContract.set(contract, {
contract,
totalAmount: amount,
operations: 1,
firstPeriod: row.period,
lastPeriod: row.period,
counterparties: new Set(counterparty ? [counterparty] : []),
sourceRefs: new Set(sourceRefs),
qualityFlags
});
continue;
}
current.totalAmount += amount;
current.operations += 1;
if ((row.period ?? "") < (current.firstPeriod ?? "")) {
current.firstPeriod = row.period;
}
if ((row.period ?? "") > (current.lastPeriod ?? "")) {
current.lastPeriod = row.period;
}
if (counterparty) {
current.counterparties.add(counterparty);
} else {
current.qualityFlags.add("counterparty_not_reliably_resolved");
}
for (const ref of sourceRefs) {
current.sourceRefs.add(ref);
}
}
return Array.from(byContract.values())
.map((item) => ({
contract: item.contract,
totalAmount: item.totalAmount,
operations: item.operations,
firstPeriod: item.firstPeriod,
lastPeriod: item.lastPeriod,
counterparties: Array.from(item.counterparties).slice(0, 2),
sourceRefs: Array.from(item.sourceRefs).slice(0, 3),
category: classifyOpenContractCategory(item.contract, Array.from(item.counterparties), Array.from(item.qualityFlags)),
qualityFlags: Array.from(item.qualityFlags)
}))
.sort((left, right) => {
if (right.totalAmount !== left.totalAmount) {
return right.totalAmount - left.totalAmount;
}
if (right.operations !== left.operations) {
return right.operations - left.operations;
}
return left.contract.localeCompare(right.contract);
});
}
export function composeFactualReply(
intent: AddressIntent,
rows: ComposeStageRow[],
@@ -2904,34 +3312,217 @@ export function composeFactualReply(
};
}
if (intent === "list_open_contracts") {
const contracts = contractCandidatesFromRows(rows);
const counterparties = buildCounterpartyRiskAggregate(rows);
const lines = [
"Проверил потенциальные разрывы во взаиморасчетах (платежи без закрытия и документы без оплат).",
`Строк движения: ${rows.length}.`,
`Договорных кандидатов: ${contracts.length}.`
if (intent === "open_contracts_confirmed_as_of_date") {
const asOfDate = resolvePayablesAsOfDate(options);
const confirmedContracts = buildOpenContractConfirmedBalanceAggregate(rows, asOfDate);
const periodFrom = normalizeIsoDateOnly(options.periodFrom);
const periodTo = normalizeIsoDateOnly(options.periodTo);
const commercialContracts = confirmedContracts.filter((item) => item.category === "commercial");
const specialContracts = confirmedContracts.filter((item) => item.category !== "commercial");
const uniqueContracts = uniqueStrings(confirmedContracts.map((item) => item.contract));
const commercialReceivables = commercialContracts.filter((item) => item.settlementKind === "receivable");
const commercialPayables = commercialContracts.filter((item) => item.settlementKind === "payable");
const commercialAdvances = commercialContracts.filter(
(item) => item.settlementKind === "advance_issued" || item.settlementKind === "advance_received"
);
const commercialOther = commercialContracts.filter(
(item) => item.settlementKind === "other_receivable" || item.settlementKind === "other_payable"
);
const sumConfirmedAmount = (items: OpenContractConfirmedAggregate[]): number =>
items.reduce((sum, item) => sum + item.confirmedAmount, 0);
const commercialTotal = sumConfirmedAmount(commercialContracts);
const specialTotal = sumConfirmedAmount(specialContracts);
const periodScopeLine =
!options.asOfDate && (periodFrom || periodTo)
? `- Период анализа: ${formatDateRu(periodFrom ?? "...")}..${formatDateRu(periodTo ?? "...")}.`
: null;
const renderConfirmedContractLines = (
items: OpenContractConfirmedAggregate[],
includeSpecialReason: boolean
): string[] =>
items.slice(0, 12).map((item, index) => {
const counterpartyLabel = item.counterparty ?? "контрагент не определен";
const accountsLabel = item.accounts.length > 0 ? ` | счета: ${item.accounts.join("; ")}` : "";
const evidenceLabel =
item.sourceRefs.length > 0 ? ` | основное основание: ${item.sourceRefs[0]}` : "";
const refsLabel =
item.sourceRefs.length > 1 ? ` | source refs: ${item.sourceRefs.slice(1, 3).join("; ")}` : "";
const specialReasonLabel = includeSpecialReason
? ` | причина вынесения: ${summarizeOpenContractSpecialReason(item)}`
: "";
return `${index + 1}. ${item.contract} | контрагент: ${counterpartyLabel} | подтвержденный открытый остаток: ${formatMoneyRub(item.confirmedAmount)} | тип остатка: ${openContractSettlementKindLabel(item.settlementKind)} | операций: ${formatNumberWithDots(item.operations)}${item.lastPeriod ? ` | последнее движение: ${item.lastPeriod}` : ""}${accountsLabel}${evidenceLabel}${refsLabel}${specialReasonLabel}`;
});
const lines: string[] = [
`Собран подтвержденный срез открытых договоров на ${formatDateRu(asOfDate)}.`,
`Коммерческие договорные позиции: ${formatNumberWithDots(commercialContracts.length)} на ${formatMoneyRub(commercialTotal)}.`,
`Финансовые/спорные позиции: ${formatNumberWithDots(specialContracts.length)} на ${formatMoneyRub(specialTotal)}.`,
"",
"Блок 1. Статус результата",
"- Результат: подтвержденный срез договоров с открытыми взаиморасчетами на дату.",
"- База ответа: остатки по счетам 60/62/76 с договорной аналитикой, без эвристического shortlist.",
"- Единица ответа: одна строка = один договор, один контрагент и один тип открытого остатка."
];
if (contracts.length > 0) {
lines.push(...contracts.slice(0, 8).map((item, index) => `${index + 1}. ${item}`));
lines.push("");
lines.push("Блок 2. Что учтено");
lines.push(`- Дата среза: ${formatDateRu(asOfDate)}.`);
if (periodScopeLine) {
lines.push(periodScopeLine);
}
lines.push("- Дефолтная бизнес-дефиниция: открыт договор, по которому на дату есть ненулевой остаток взаиморасчетов.");
lines.push("- Контур: остатки по счетам 60/62/76.");
lines.push("- Смешанные экономические смыслы не склеиваются: дебиторка, кредиторка, авансы и прочие остатки показаны раздельно.");
lines.push("");
lines.push("Блок 3. Сводка");
lines.push(`- Строк в выборке: ${formatNumberWithDots(rows.length)}.`);
lines.push(`- Уникальных договоров: ${formatNumberWithDots(uniqueContracts.length)}.`);
lines.push(`- Подтвержденных договорных позиций: ${formatNumberWithDots(confirmedContracts.length)}.`);
lines.push(
`- Коммерческая дебиторка: ${formatNumberWithDots(commercialReceivables.length)} на ${formatMoneyRub(sumConfirmedAmount(commercialReceivables))}.`
);
lines.push(
`- Коммерческая кредиторка: ${formatNumberWithDots(commercialPayables.length)} на ${formatMoneyRub(sumConfirmedAmount(commercialPayables))}.`
);
lines.push(
`- Коммерческие авансы: ${formatNumberWithDots(commercialAdvances.length)} на ${formatMoneyRub(sumConfirmedAmount(commercialAdvances))}.`
);
lines.push(
`- Прочие расчеты по 76: ${formatNumberWithDots(commercialOther.length)} на ${formatMoneyRub(sumConfirmedAmount(commercialOther))}.`
);
lines.push(`- Финансовые/спорные позиции: ${formatNumberWithDots(specialContracts.length)} на ${formatMoneyRub(specialTotal)}.`);
if (commercialReceivables.length > 0) {
lines.push("");
lines.push("Блок 4. Коммерческие договоры с дебиторской задолженностью");
lines.push(...renderConfirmedContractLines(commercialReceivables, false));
}
if (commercialPayables.length > 0) {
lines.push("");
lines.push("Блок 5. Коммерческие договоры с кредиторской задолженностью");
lines.push(...renderConfirmedContractLines(commercialPayables, false));
}
if (commercialAdvances.length > 0) {
lines.push("");
lines.push("Блок 6. Коммерческие авансы");
lines.push(...renderConfirmedContractLines(commercialAdvances, false));
}
if (commercialOther.length > 0) {
lines.push("");
lines.push("Блок 7. Прочие расчеты по 76");
lines.push(...renderConfirmedContractLines(commercialOther, false));
}
if (specialContracts.length > 0) {
lines.push("");
lines.push("Блок 8. Финансовые/спорные позиции");
lines.push(...renderConfirmedContractLines(specialContracts, true));
}
if (confirmedContracts.length === 0) {
lines.push("");
lines.push("Блок 4. Подтвержденные позиции");
lines.push("- На дату среза подтвержденные договоры с открытыми взаиморасчетами не найдены.");
}
return {
responseType: "FACTUAL_LIST",
text: joinLines(lines),
semantics: {
result_mode: "confirmed_balance",
evidence_strength: "strong",
balance_confirmed: true
}
};
}
if (intent === "list_open_contracts") {
const contracts = buildOpenContractRiskAggregate(rows);
const counterparties = buildCounterpartyRiskAggregate(rows);
const asOfDate = normalizeIsoDateOnly(options.asOfDate ?? options.periodTo ?? options.periodFrom);
const periodFrom = normalizeIsoDateOnly(options.periodFrom);
const periodTo = normalizeIsoDateOnly(options.periodTo);
const commercialContracts = contracts.filter((item) => item.category === "commercial");
const specialContracts = contracts.filter((item) => item.category !== "commercial");
const commercialTotal = commercialContracts.reduce((sum, item) => sum + item.totalAmount, 0);
const lines: string[] = [
`Итого по предварительному срезу открытых договоров${asOfDate ? ` на ${formatDateRu(asOfDate)}` : ""}: ${formatNumberWithDots(commercialContracts.length)} коммерческих договоров на ${formatMoneyRub(commercialTotal)}.`,
"",
"Блок 1. Статус результата",
"- Результат: предварительный список договоров с возможными незакрытыми расчетами.",
"- Перед финансовым решением нужна сверка карточек договоров и взаиморасчетов в 1С.",
"",
"Блок 2. Что учтено",
...(asOfDate
? [`- Дата среза: ${formatDateRu(asOfDate)}.`]
: periodFrom || periodTo
? [`- Период анализа: ${formatDateRu(periodFrom ?? "...")}..${formatDateRu(periodTo ?? "...")}.`]
: []),
"- Контур: движения по счетам 60/62/76 и договорная аналитика.",
"",
"Блок 3. Сводка",
`- Строк в выборке: ${formatNumberWithDots(rows.length)}.`,
`- Договоров-кандидатов всего: ${formatNumberWithDots(contracts.length)}.`,
`- Основной список (коммерческие): ${formatNumberWithDots(commercialContracts.length)}.`,
`- Вынесено в финансовые/спорные: ${formatNumberWithDots(specialContracts.length)}.`
];
if (commercialContracts.length > 0) {
lines.push("");
lines.push("Блок 4. Основной список (коммерческие договоры)");
lines.push(
...commercialContracts.slice(0, 10).map((item, index) => {
const counterpartiesLabel =
item.counterparties.length > 0 ? item.counterparties.join("; ") : "контрагент не определен";
const sourceRefsSuffix =
item.sourceRefs.length > 0 ? ` | source refs: ${item.sourceRefs.slice(0, 2).join("; ")}` : "";
return `${index + 1}. ${item.contract} | контрагент: ${counterpartiesLabel} | сумма возможного открытого остатка: ${formatMoneyRub(item.totalAmount)} | операций: ${formatNumberWithDots(item.operations)}${item.lastPeriod ? ` | последнее движение: ${item.lastPeriod}` : ""} | почему в списке: есть признаки незакрытых расчетов на дату${sourceRefsSuffix}`;
})
);
if (specialContracts.length > 0) {
lines.push("");
lines.push("Блок 5. Финансовые/спорные позиции (вынесены отдельно)");
lines.push(
...specialContracts.slice(0, 8).map((item, index) => {
const counterpartiesLabel =
item.counterparties.length > 0 ? item.counterparties.join("; ") : "контрагент не определен";
const sourceRefsSuffix =
item.sourceRefs.length > 0 ? ` | source refs: ${item.sourceRefs.slice(0, 2).join("; ")}` : "";
return `${index + 1}. ${item.contract} | контрагент: ${counterpartiesLabel} | сумма сигнала: ${formatMoneyRub(item.totalAmount)} | причина вынесения: ${summarizeOpenContractSpecialReason(item)}${sourceRefsSuffix}`;
})
);
}
} else if (counterparties.length > 0) {
lines.push(`Контрагентов с сигналом незакрытых хвостов: ${counterparties.length}.`);
lines.push("");
lines.push("Блок 4. Контрагенты с сигналом незакрытых расчетов");
lines.push(`- Контрагентов с сигналом: ${formatNumberWithDots(counterparties.length)}.`);
lines.push(
...counterparties
.slice(0, 8)
.map(
(item, index) =>
`${index + 1}. ${item.name} | сумма сигнала: ${formatMoney(item.totalAmount)} | операций: ${item.operations}${item.lastPeriod ? ` | последнее движение: ${item.lastPeriod}` : ""}`
`${index + 1}. ${item.name} | сумма сигнала: ${formatMoneyRub(item.totalAmount)} | операций: ${formatNumberWithDots(item.operations)}${item.lastPeriod ? ` | последнее движение: ${item.lastPeriod}` : ""}`
)
);
lines.push("Договорные якоря в этом live-срезе не выделены, поэтому показан контрагентный рейтинг риска.");
lines.push("- Договорные реквизиты выделены недостаточно надежно, поэтому показан контрагентный список для проверки.");
} else {
lines.push("Договорные якоря в live-строках не выделены; показаны связанные движения как fallback.");
lines.push("");
lines.push("Блок 4. Позиции не выделены");
lines.push("- По текущему live-срезу не удалось выделить договоры с достаточным качеством идентификации.");
lines.push("Блок 5. Примеры исходных строк");
lines.push(...formatTopRows(rows, 6));
}
return {
responseType: "FACTUAL_LIST",
text: lines.join("\n")
text: joinLines(lines),
semantics: {
result_mode: "heuristic_candidates",
evidence_strength: contracts.length > 0 || counterparties.length > 0 ? "medium" : "weak",
balance_confirmed: false
}
};
}
@@ -367,6 +367,15 @@ function mergeFollowupFilters(
const merged: AddressFilterSet = { ...current };
const reasons: string[] = [];
if (!followupContext) {
if ((intent === "list_open_contracts" || intent === "open_contracts_confirmed_as_of_date") && !toNonEmptyString(merged.as_of_date)) {
const periodToForOpenContracts = toNonEmptyString(merged.period_to);
const periodFromForOpenContracts = toNonEmptyString(merged.period_from);
const derivedAsOfDate = periodToForOpenContracts ?? periodFromForOpenContracts;
if (derivedAsOfDate) {
merged.as_of_date = derivedAsOfDate;
reasons.push("as_of_date_derived_from_period_for_open_contracts");
}
}
return { filters: merged, reasons };
}
@@ -470,6 +479,7 @@ function mergeFollowupFilters(
if (
intent === "open_items_by_counterparty_or_contract" ||
intent === "list_open_contracts" ||
intent === "open_contracts_confirmed_as_of_date" ||
intent === "payables_confirmed_as_of_date" ||
intent === "receivables_confirmed_as_of_date" ||
intent === "vat_payable_confirmed_as_of_date"
@@ -550,6 +560,7 @@ function mergeFollowupFilters(
const asOfPrimaryIntent =
intent === "account_balance_snapshot" ||
intent === "documents_forming_balance" ||
intent === "open_contracts_confirmed_as_of_date" ||
intent === "payables_confirmed_as_of_date" ||
intent === "receivables_confirmed_as_of_date" ||
intent === "vat_payable_confirmed_as_of_date";
@@ -587,6 +598,16 @@ function mergeFollowupFilters(
reasons.push("period_from_followup_context");
}
if ((intent === "list_open_contracts" || intent === "open_contracts_confirmed_as_of_date") && !toNonEmptyString(merged.as_of_date)) {
const periodToForOpenContracts = toNonEmptyString(merged.period_to);
const periodFromForOpenContracts = toNonEmptyString(merged.period_from);
const derivedAsOfDate = periodToForOpenContracts ?? periodFromForOpenContracts;
if (derivedAsOfDate) {
merged.as_of_date = derivedAsOfDate;
reasons.push("as_of_date_derived_from_period_for_open_contracts");
}
}
return { filters: merged, reasons };
}
@@ -594,6 +615,7 @@ function resolveMissingRequiredFilters(intent: AddressIntent, filters: AddressFi
const requiredByIntent: Record<string, Array<keyof AddressFilterSet>> = {
account_balance_snapshot: ["account", "as_of_date"],
documents_forming_balance: ["account", "as_of_date"],
open_contracts_confirmed_as_of_date: ["as_of_date"],
payables_confirmed_as_of_date: ["as_of_date"],
receivables_confirmed_as_of_date: ["as_of_date"],
vat_payable_confirmed_as_of_date: ["as_of_date"],
@@ -192,6 +192,7 @@ function inferAggregationProfile(intent: AddressIntent, shape: AddressQueryShape
if (
intent === "account_balance_snapshot" ||
intent === "documents_forming_balance" ||
intent === "open_contracts_confirmed_as_of_date" ||
intent === "payables_confirmed_as_of_date" ||
intent === "receivables_confirmed_as_of_date" ||
intent === "vat_payable_confirmed_as_of_date" ||