ДОМЕНЫ - ВОПРОСЫ - ОТКРЫТЫЕ ДОГОВОРА - Усилить business-view exact открытых договоров: разрез по типам остатков и quality gates
This commit is contained in:
@@ -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" ||
|
||||
|
||||
Reference in New Issue
Block a user