ДОМЕНЫ - ВОПРОСЫ - ОТКРЫТЫЕ ДОГОВОРА - Усилить business-view exact открытых договоров: разрез по типам остатков и quality gates
This commit is contained in:
@@ -26,6 +26,7 @@ export interface AddressCapabilityRouteDecision {
|
||||
const COMPUTE_EXACT_INTENTS = new Set<AddressIntent>([
|
||||
"account_balance_snapshot",
|
||||
"documents_forming_balance",
|
||||
"open_contracts_confirmed_as_of_date",
|
||||
"payables_confirmed_as_of_date",
|
||||
"receivables_confirmed_as_of_date",
|
||||
"vat_payable_confirmed_as_of_date",
|
||||
@@ -64,6 +65,9 @@ function defaultCapabilityId(intent: AddressIntent): string {
|
||||
if (intent === "receivables_confirmed_as_of_date") {
|
||||
return "confirmed_receivables_as_of_date";
|
||||
}
|
||||
if (intent === "open_contracts_confirmed_as_of_date") {
|
||||
return "confirmed_open_contracts_as_of_date";
|
||||
}
|
||||
if (intent === "vat_payable_confirmed_as_of_date") {
|
||||
return "confirmed_vat_payable_as_of_date";
|
||||
}
|
||||
@@ -106,6 +110,14 @@ function resolveCapabilityEnabled(intent: AddressIntent): { enabled: boolean; re
|
||||
: "receivables_confirmed_route_disabled_by_flag"
|
||||
};
|
||||
}
|
||||
if (intent === "open_contracts_confirmed_as_of_date") {
|
||||
return {
|
||||
enabled: FEATURE_ASSISTANT_ROUTE_BALANCE_EXACT_V1,
|
||||
reason: FEATURE_ASSISTANT_ROUTE_BALANCE_EXACT_V1
|
||||
? "open_contracts_confirmed_route_enabled"
|
||||
: "open_contracts_confirmed_route_disabled_by_flag"
|
||||
};
|
||||
}
|
||||
if (intent === "vat_payable_confirmed_as_of_date") {
|
||||
return {
|
||||
enabled: FEATURE_ASSISTANT_ROUTE_BALANCE_EXACT_V1,
|
||||
|
||||
@@ -933,6 +933,9 @@ function requiredFiltersByIntent(intent: AddressIntent): Array<keyof AddressFilt
|
||||
if (intent === "receivables_confirmed_as_of_date") {
|
||||
return ["as_of_date"];
|
||||
}
|
||||
if (intent === "open_contracts_confirmed_as_of_date") {
|
||||
return ["as_of_date"];
|
||||
}
|
||||
if (intent === "vat_payable_confirmed_as_of_date") {
|
||||
return ["as_of_date"];
|
||||
}
|
||||
@@ -956,6 +959,7 @@ function usesAsOfPrimaryWindow(intent: AddressIntent): boolean {
|
||||
return (
|
||||
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"
|
||||
@@ -979,7 +983,9 @@ export function extractAddressFilters(userMessage: string, intent: AddressIntent
|
||||
sort: "period_desc"
|
||||
};
|
||||
if (!isManagementProfileIntent) {
|
||||
filters.limit = 20;
|
||||
if (intent !== "open_contracts_confirmed_as_of_date") {
|
||||
filters.limit = 20;
|
||||
}
|
||||
}
|
||||
const warnings: string[] = [];
|
||||
const explicitAsOfDate = extractAsOfDate(text);
|
||||
|
||||
@@ -1661,7 +1661,7 @@ export function resolveAddressIntent(userMessage: string): AddressIntentResoluti
|
||||
|
||||
if (hasOpenContractsListSignal(text)) {
|
||||
return {
|
||||
intent: "list_open_contracts",
|
||||
intent: "open_contracts_confirmed_as_of_date",
|
||||
confidence: "medium",
|
||||
reasons: ["open_contract_signal_detected"]
|
||||
};
|
||||
@@ -1844,7 +1844,7 @@ export function resolveAddressIntent(userMessage: string): AddressIntentResoluti
|
||||
|
||||
if (hasAny(text, OPEN_CONTRACTS_HINTS) && (text.includes("договор") || text.includes("контракт") || text.includes("contract"))) {
|
||||
return {
|
||||
intent: "list_open_contracts",
|
||||
intent: "open_contracts_confirmed_as_of_date",
|
||||
confidence: "medium",
|
||||
reasons: ["open_contract_signal_detected"]
|
||||
};
|
||||
|
||||
@@ -962,6 +962,7 @@ function shouldAttemptCounterpartyCatalogResolution(intent: AddressIntent, filte
|
||||
intent === "list_documents_by_counterparty" ||
|
||||
intent === "bank_operations_by_counterparty" ||
|
||||
intent === "open_items_by_counterparty_or_contract" ||
|
||||
intent === "open_contracts_confirmed_as_of_date" ||
|
||||
intent === "list_payables_counterparties" ||
|
||||
intent === "payables_confirmed_as_of_date" ||
|
||||
intent === "receivables_confirmed_as_of_date" ||
|
||||
@@ -1291,6 +1292,7 @@ function isCounterpartyRiskIntent(intent: AddressIntent): boolean {
|
||||
return (
|
||||
intent === "list_receivables_counterparties" ||
|
||||
intent === "list_payables_counterparties" ||
|
||||
intent === "open_contracts_confirmed_as_of_date" ||
|
||||
intent === "payables_confirmed_as_of_date" ||
|
||||
intent === "receivables_confirmed_as_of_date" ||
|
||||
intent === "list_open_contracts" ||
|
||||
@@ -1311,6 +1313,7 @@ function isConfirmedBalanceIntent(intent: AddressIntent): boolean {
|
||||
return (
|
||||
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" ||
|
||||
@@ -1365,6 +1368,9 @@ function resolveRequestedResultMode(intent: AddressIntent, filters: AddressFilte
|
||||
if (isConfirmedBalanceIntent(intent)) {
|
||||
return "confirmed_balance";
|
||||
}
|
||||
if (intent === "list_open_contracts") {
|
||||
return "heuristic_candidates";
|
||||
}
|
||||
if (isHeuristicCandidatesIntent(intent)) {
|
||||
const asOfDateBasis = resolveAsOfDateBasis(filters);
|
||||
if (asOfDateBasis === "explicit_as_of_date" || asOfDateBasis === "period_end" || asOfDateBasis === "period_range") {
|
||||
@@ -1526,6 +1532,10 @@ function enforceStrictAccountScopeForIntent(
|
||||
plan: AddressRecipeExecutionPlan,
|
||||
intent: AddressIntent
|
||||
): AddressRecipeExecutionPlan {
|
||||
if (intent === "list_open_contracts" && plan.recipe.recipe_id === "address_open_items_by_party_or_contract_v1") {
|
||||
return plan;
|
||||
}
|
||||
|
||||
const strictScopeIntents: AddressIntent[] = [
|
||||
"list_receivables_counterparties",
|
||||
"list_open_contracts",
|
||||
@@ -2154,6 +2164,8 @@ function buildLimitedOffers(input: {
|
||||
offers.push("показать контрагентов с максимальными хвостами дебиторки по 62/76");
|
||||
} else if (input.intent === "receivables_confirmed_as_of_date") {
|
||||
offers.push("показать подтвержденный реестр открытой дебиторской задолженности на дату среза по 62/76");
|
||||
} else if (input.intent === "open_contracts_confirmed_as_of_date") {
|
||||
offers.push("показать подтвержденный реестр договоров с открытыми взаиморасчетами на дату по 60/62/76");
|
||||
} else if (input.intent === "vat_payable_confirmed_as_of_date") {
|
||||
offers.push("показать подтвержденную сумму НДС к уплате на дату среза по счетам 68*");
|
||||
} else if (input.intent === "vat_liability_confirmed_for_tax_period") {
|
||||
@@ -2208,6 +2220,7 @@ function buildLimitedIntentSignalLine(input: {
|
||||
bank_operations_by_contract: "Сигнал запроса: нужен срез банковских операций по договору.",
|
||||
open_items_by_counterparty_or_contract: "Сигнал запроса: нужен контроль незакрытых взаиморасчетов.",
|
||||
list_open_contracts: "Сигнал запроса: нужен список незакрытых договоров на дату.",
|
||||
open_contracts_confirmed_as_of_date: "Сигнал запроса: нужен подтвержденный список договоров с открытыми взаиморасчетами на дату.",
|
||||
list_receivables_counterparties: "Сигнал запроса: нужен ранжированный список должников.",
|
||||
list_payables_counterparties: "Сигнал запроса: нужен ранжированный список кредиторов.",
|
||||
receivables_confirmed_as_of_date: "Сигнал запроса: нужен подтвержденный срез дебиторской задолженности на дату.",
|
||||
@@ -3630,6 +3643,7 @@ export class AddressQueryService {
|
||||
const allowConfirmedAsOfZeroSnapshot =
|
||||
filteredRows.length === 0 &&
|
||||
(composeIntent === "vat_payable_confirmed_as_of_date" ||
|
||||
composeIntent === "open_contracts_confirmed_as_of_date" ||
|
||||
composeIntent === "payables_confirmed_as_of_date" ||
|
||||
composeIntent === "receivables_confirmed_as_of_date") &&
|
||||
(stageStatus === "no_raw_rows" || stageStatus === "materialized_but_filtered_out_by_recipe") &&
|
||||
|
||||
@@ -72,6 +72,48 @@ const RECEIVABLES_CONFIRMED_AS_OF_QUERY_TEMPLATE = `
|
||||
Сумма __ORDER_DIRECTION__
|
||||
`;
|
||||
|
||||
const OPEN_CONTRACTS_CONFIRMED_AS_OF_QUERY_TEMPLATE = `
|
||||
ВЫБРАТЬ ПЕРВЫЕ __LIMIT__
|
||||
__AS_OF_EXPR__ КАК Период,
|
||||
"Остатки на дату" КАК Регистратор,
|
||||
ПРЕДСТАВЛЕНИЕ(Остатки.Счет) КАК СчетДт,
|
||||
"" КАК СчетКт,
|
||||
Остатки.СуммаРазвернутыйОстатокДт КАК Сумма,
|
||||
ПРЕДСТАВЛЕНИЕ(Остатки.Субконто1) КАК СубконтоДт1,
|
||||
ПРЕДСТАВЛЕНИЕ(Остатки.Субконто2) КАК СубконтоДт2,
|
||||
ПРЕДСТАВЛЕНИЕ(Остатки.Субконто3) КАК СубконтоДт3,
|
||||
ПРЕДСТАВЛЕНИЕ(Остатки.Субконто1) КАК СубконтоКт1,
|
||||
ПРЕДСТАВЛЕНИЕ(Остатки.Субконто2) КАК СубконтоКт2,
|
||||
ПРЕДСТАВЛЕНИЕ(Остатки.Субконто3) КАК СубконтоКт3,
|
||||
ПРЕДСТАВЛЕНИЕ(Остатки.Организация) КАК Организация
|
||||
ИЗ
|
||||
РегистрБухгалтерии.Хозрасчетный.Остатки(__AS_OF_EXPR__, , , ) КАК Остатки
|
||||
ГДЕ
|
||||
Остатки.СуммаРазвернутыйОстатокДт > 0
|
||||
И (__OPEN_CONTRACT_ACCOUNTS_MATCH__)
|
||||
ОБЪЕДИНИТЬ ВСЕ
|
||||
ВЫБРАТЬ ПЕРВЫЕ __LIMIT__
|
||||
__AS_OF_EXPR__ КАК Период,
|
||||
"Остатки на дату" КАК Регистратор,
|
||||
"" КАК СчетДт,
|
||||
ПРЕДСТАВЛЕНИЕ(Остатки.Счет) КАК СчетКт,
|
||||
Остатки.СуммаРазвернутыйОстатокКт КАК Сумма,
|
||||
ПРЕДСТАВЛЕНИЕ(Остатки.Субконто1) КАК СубконтоДт1,
|
||||
ПРЕДСТАВЛЕНИЕ(Остатки.Субконто2) КАК СубконтоДт2,
|
||||
ПРЕДСТАВЛЕНИЕ(Остатки.Субконто3) КАК СубконтоДт3,
|
||||
ПРЕДСТАВЛЕНИЕ(Остатки.Субконто1) КАК СубконтоКт1,
|
||||
ПРЕДСТАВЛЕНИЕ(Остатки.Субконто2) КАК СубконтоКт2,
|
||||
ПРЕДСТАВЛЕНИЕ(Остатки.Субконто3) КАК СубконтоКт3,
|
||||
ПРЕДСТАВЛЕНИЕ(Остатки.Организация) КАК Организация
|
||||
ИЗ
|
||||
РегистрБухгалтерии.Хозрасчетный.Остатки(__AS_OF_EXPR__, , , ) КАК Остатки
|
||||
ГДЕ
|
||||
Остатки.СуммаРазвернутыйОстатокКт > 0
|
||||
И (__OPEN_CONTRACT_ACCOUNTS_MATCH__)
|
||||
УПОРЯДОЧИТЬ ПО
|
||||
Сумма __ORDER_DIRECTION__
|
||||
`;
|
||||
|
||||
const VAT_PAYABLE_CONFIRMED_AS_OF_QUERY_TEMPLATE = `
|
||||
ВЫБРАТЬ ПЕРВЫЕ __LIMIT__
|
||||
__AS_OF_EXPR__ КАК Период,
|
||||
@@ -634,6 +676,17 @@ const BASE_RECIPES: AddressRecipeDefinition[] = [
|
||||
account_scope_mode: "preferred",
|
||||
query_template: "vat_liability_confirmed_tax_period_profile"
|
||||
},
|
||||
{
|
||||
recipe_id: "address_open_contracts_confirmed_as_of_date_v1",
|
||||
intent: "open_contracts_confirmed_as_of_date",
|
||||
purpose: "Build confirmed snapshot of contracts with open settlements as-of date from balances on accounts 60/62/76",
|
||||
required_filters: ["as_of_date"],
|
||||
optional_filters: ["period_from", "period_to", "organization", "counterparty", "contract", "limit", "sort"],
|
||||
default_limit: 400,
|
||||
account_scope: ["60", "62", "76"],
|
||||
account_scope_mode: "strict",
|
||||
query_template: "open_contracts_confirmed_as_of_balance_profile"
|
||||
},
|
||||
{
|
||||
recipe_id: "address_contracts_by_counterparty_v1",
|
||||
intent: "list_contracts_by_counterparty",
|
||||
@@ -982,6 +1035,7 @@ function maxLimitForIntent(intent: AddressIntent): number {
|
||||
intent === "contract_usage_and_value" ||
|
||||
intent === "vat_payable_forecast" ||
|
||||
intent === "vat_liability_confirmed_for_tax_period" ||
|
||||
intent === "open_contracts_confirmed_as_of_date" ||
|
||||
intent === "list_contracts_by_counterparty" ||
|
||||
intent === "list_documents_by_counterparty" ||
|
||||
intent === "bank_operations_by_counterparty" ||
|
||||
@@ -1156,6 +1210,25 @@ export function buildAddressRecipePlan(
|
||||
})()
|
||||
: recipe.query_template === "contracts_by_counterparty_profile"
|
||||
? CONTRACTS_BY_COUNTERPARTY_QUERY_TEMPLATE.replaceAll("__LIMIT__", String(resolvedLimit))
|
||||
: recipe.query_template === "open_contracts_confirmed_as_of_balance_profile"
|
||||
? (() => {
|
||||
const asOfExpr =
|
||||
(typeof filters.as_of_date === "string" && filters.as_of_date.trim().length > 0
|
||||
? toDateTimeExpr(filters.as_of_date, true)
|
||||
: null) ??
|
||||
(typeof filters.period_to === "string" && filters.period_to.trim().length > 0
|
||||
? toDateTimeExpr(filters.period_to, true)
|
||||
: null) ??
|
||||
(typeof filters.period_from === "string" && filters.period_from.trim().length > 0
|
||||
? toDateTimeExpr(filters.period_from, true)
|
||||
: null) ??
|
||||
"ТЕКУЩАЯДАТА()";
|
||||
return OPEN_CONTRACTS_CONFIRMED_AS_OF_QUERY_TEMPLATE
|
||||
.replaceAll("__LIMIT__", String(resolvedLimit))
|
||||
.replaceAll("__AS_OF_EXPR__", asOfExpr)
|
||||
.replaceAll("__OPEN_CONTRACT_ACCOUNTS_MATCH__", buildAccountPrefixPredicate("Остатки.Счет", ["60", "62", "76"]))
|
||||
.replaceAll("__ORDER_DIRECTION__", resolveOrderDirection(filters.sort));
|
||||
})()
|
||||
: recipe.query_template === "payables_confirmed_as_of_balance_profile"
|
||||
? (() => {
|
||||
const asOfExpr =
|
||||
|
||||
@@ -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" ||
|
||||
|
||||
@@ -3801,6 +3801,7 @@ const ADDRESS_INTENTS_KEEP_ADDRESS_LANE = new Set([
|
||||
"counterparty_activity_lifecycle",
|
||||
"customer_revenue_and_payments",
|
||||
"supplier_payouts_profile",
|
||||
"open_contracts_confirmed_as_of_date",
|
||||
"list_open_contracts",
|
||||
"open_items_by_counterparty_or_contract",
|
||||
"list_payables_counterparties",
|
||||
|
||||
Reference in New Issue
Block a user