ДОМЕНЫ - ВОПРОСЫ - ОТКРЫТЫЕ ДОГОВОРА - Усилить business-view exact открытых договоров: разрез по типам остатков и quality gates
This commit is contained in:
parent
3e588ede81
commit
ef4222d159
|
|
@ -42,6 +42,18 @@
|
||||||
"expected_requested_result_modes": ["heuristic_candidates", "confirmed_balance"],
|
"expected_requested_result_modes": ["heuristic_candidates", "confirmed_balance"],
|
||||||
"expected_result_modes": ["heuristic_candidates", "confirmed_balance"]
|
"expected_result_modes": ["heuristic_candidates", "confirmed_balance"]
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"intent": "open_contracts_confirmed_as_of_date",
|
||||||
|
"expected_selected_recipes": ["address_open_contracts_confirmed_as_of_date_v1"],
|
||||||
|
"expected_requested_result_modes": ["confirmed_balance"],
|
||||||
|
"expected_result_modes": ["confirmed_balance"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"intent": "list_open_contracts",
|
||||||
|
"expected_selected_recipes": ["address_open_contracts_candidates_v1", "address_open_items_by_party_or_contract_v1"],
|
||||||
|
"expected_requested_result_modes": ["heuristic_candidates"],
|
||||||
|
"expected_result_modes": ["heuristic_candidates"]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"intent": "account_balance_snapshot",
|
"intent": "account_balance_snapshot",
|
||||||
"expected_selected_recipes": ["address_open_items_by_party_or_contract_v1"],
|
"expected_selected_recipes": ["address_open_items_by_party_or_contract_v1"],
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,7 @@ export interface AddressCapabilityRouteDecision {
|
||||||
const COMPUTE_EXACT_INTENTS = new Set<AddressIntent>([
|
const COMPUTE_EXACT_INTENTS = new Set<AddressIntent>([
|
||||||
"account_balance_snapshot",
|
"account_balance_snapshot",
|
||||||
"documents_forming_balance",
|
"documents_forming_balance",
|
||||||
|
"open_contracts_confirmed_as_of_date",
|
||||||
"payables_confirmed_as_of_date",
|
"payables_confirmed_as_of_date",
|
||||||
"receivables_confirmed_as_of_date",
|
"receivables_confirmed_as_of_date",
|
||||||
"vat_payable_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") {
|
if (intent === "receivables_confirmed_as_of_date") {
|
||||||
return "confirmed_receivables_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") {
|
if (intent === "vat_payable_confirmed_as_of_date") {
|
||||||
return "confirmed_vat_payable_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"
|
: "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") {
|
if (intent === "vat_payable_confirmed_as_of_date") {
|
||||||
return {
|
return {
|
||||||
enabled: FEATURE_ASSISTANT_ROUTE_BALANCE_EXACT_V1,
|
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") {
|
if (intent === "receivables_confirmed_as_of_date") {
|
||||||
return ["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") {
|
if (intent === "vat_payable_confirmed_as_of_date") {
|
||||||
return ["as_of_date"];
|
return ["as_of_date"];
|
||||||
}
|
}
|
||||||
|
|
@ -956,6 +959,7 @@ function usesAsOfPrimaryWindow(intent: AddressIntent): boolean {
|
||||||
return (
|
return (
|
||||||
intent === "open_items_by_counterparty_or_contract" ||
|
intent === "open_items_by_counterparty_or_contract" ||
|
||||||
intent === "list_open_contracts" ||
|
intent === "list_open_contracts" ||
|
||||||
|
intent === "open_contracts_confirmed_as_of_date" ||
|
||||||
intent === "payables_confirmed_as_of_date" ||
|
intent === "payables_confirmed_as_of_date" ||
|
||||||
intent === "receivables_confirmed_as_of_date" ||
|
intent === "receivables_confirmed_as_of_date" ||
|
||||||
intent === "vat_payable_confirmed_as_of_date"
|
intent === "vat_payable_confirmed_as_of_date"
|
||||||
|
|
@ -979,8 +983,10 @@ export function extractAddressFilters(userMessage: string, intent: AddressIntent
|
||||||
sort: "period_desc"
|
sort: "period_desc"
|
||||||
};
|
};
|
||||||
if (!isManagementProfileIntent) {
|
if (!isManagementProfileIntent) {
|
||||||
|
if (intent !== "open_contracts_confirmed_as_of_date") {
|
||||||
filters.limit = 20;
|
filters.limit = 20;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
const warnings: string[] = [];
|
const warnings: string[] = [];
|
||||||
const explicitAsOfDate = extractAsOfDate(text);
|
const explicitAsOfDate = extractAsOfDate(text);
|
||||||
const explicitAsOfDateWithCue = extractAsOfDateWithCue(text);
|
const explicitAsOfDateWithCue = extractAsOfDateWithCue(text);
|
||||||
|
|
|
||||||
|
|
@ -1661,7 +1661,7 @@ export function resolveAddressIntent(userMessage: string): AddressIntentResoluti
|
||||||
|
|
||||||
if (hasOpenContractsListSignal(text)) {
|
if (hasOpenContractsListSignal(text)) {
|
||||||
return {
|
return {
|
||||||
intent: "list_open_contracts",
|
intent: "open_contracts_confirmed_as_of_date",
|
||||||
confidence: "medium",
|
confidence: "medium",
|
||||||
reasons: ["open_contract_signal_detected"]
|
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"))) {
|
if (hasAny(text, OPEN_CONTRACTS_HINTS) && (text.includes("договор") || text.includes("контракт") || text.includes("contract"))) {
|
||||||
return {
|
return {
|
||||||
intent: "list_open_contracts",
|
intent: "open_contracts_confirmed_as_of_date",
|
||||||
confidence: "medium",
|
confidence: "medium",
|
||||||
reasons: ["open_contract_signal_detected"]
|
reasons: ["open_contract_signal_detected"]
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -962,6 +962,7 @@ function shouldAttemptCounterpartyCatalogResolution(intent: AddressIntent, filte
|
||||||
intent === "list_documents_by_counterparty" ||
|
intent === "list_documents_by_counterparty" ||
|
||||||
intent === "bank_operations_by_counterparty" ||
|
intent === "bank_operations_by_counterparty" ||
|
||||||
intent === "open_items_by_counterparty_or_contract" ||
|
intent === "open_items_by_counterparty_or_contract" ||
|
||||||
|
intent === "open_contracts_confirmed_as_of_date" ||
|
||||||
intent === "list_payables_counterparties" ||
|
intent === "list_payables_counterparties" ||
|
||||||
intent === "payables_confirmed_as_of_date" ||
|
intent === "payables_confirmed_as_of_date" ||
|
||||||
intent === "receivables_confirmed_as_of_date" ||
|
intent === "receivables_confirmed_as_of_date" ||
|
||||||
|
|
@ -1291,6 +1292,7 @@ function isCounterpartyRiskIntent(intent: AddressIntent): boolean {
|
||||||
return (
|
return (
|
||||||
intent === "list_receivables_counterparties" ||
|
intent === "list_receivables_counterparties" ||
|
||||||
intent === "list_payables_counterparties" ||
|
intent === "list_payables_counterparties" ||
|
||||||
|
intent === "open_contracts_confirmed_as_of_date" ||
|
||||||
intent === "payables_confirmed_as_of_date" ||
|
intent === "payables_confirmed_as_of_date" ||
|
||||||
intent === "receivables_confirmed_as_of_date" ||
|
intent === "receivables_confirmed_as_of_date" ||
|
||||||
intent === "list_open_contracts" ||
|
intent === "list_open_contracts" ||
|
||||||
|
|
@ -1311,6 +1313,7 @@ function isConfirmedBalanceIntent(intent: AddressIntent): boolean {
|
||||||
return (
|
return (
|
||||||
intent === "account_balance_snapshot" ||
|
intent === "account_balance_snapshot" ||
|
||||||
intent === "documents_forming_balance" ||
|
intent === "documents_forming_balance" ||
|
||||||
|
intent === "open_contracts_confirmed_as_of_date" ||
|
||||||
intent === "payables_confirmed_as_of_date" ||
|
intent === "payables_confirmed_as_of_date" ||
|
||||||
intent === "receivables_confirmed_as_of_date" ||
|
intent === "receivables_confirmed_as_of_date" ||
|
||||||
intent === "vat_payable_confirmed_as_of_date" ||
|
intent === "vat_payable_confirmed_as_of_date" ||
|
||||||
|
|
@ -1365,6 +1368,9 @@ function resolveRequestedResultMode(intent: AddressIntent, filters: AddressFilte
|
||||||
if (isConfirmedBalanceIntent(intent)) {
|
if (isConfirmedBalanceIntent(intent)) {
|
||||||
return "confirmed_balance";
|
return "confirmed_balance";
|
||||||
}
|
}
|
||||||
|
if (intent === "list_open_contracts") {
|
||||||
|
return "heuristic_candidates";
|
||||||
|
}
|
||||||
if (isHeuristicCandidatesIntent(intent)) {
|
if (isHeuristicCandidatesIntent(intent)) {
|
||||||
const asOfDateBasis = resolveAsOfDateBasis(filters);
|
const asOfDateBasis = resolveAsOfDateBasis(filters);
|
||||||
if (asOfDateBasis === "explicit_as_of_date" || asOfDateBasis === "period_end" || asOfDateBasis === "period_range") {
|
if (asOfDateBasis === "explicit_as_of_date" || asOfDateBasis === "period_end" || asOfDateBasis === "period_range") {
|
||||||
|
|
@ -1526,6 +1532,10 @@ function enforceStrictAccountScopeForIntent(
|
||||||
plan: AddressRecipeExecutionPlan,
|
plan: AddressRecipeExecutionPlan,
|
||||||
intent: AddressIntent
|
intent: AddressIntent
|
||||||
): AddressRecipeExecutionPlan {
|
): AddressRecipeExecutionPlan {
|
||||||
|
if (intent === "list_open_contracts" && plan.recipe.recipe_id === "address_open_items_by_party_or_contract_v1") {
|
||||||
|
return plan;
|
||||||
|
}
|
||||||
|
|
||||||
const strictScopeIntents: AddressIntent[] = [
|
const strictScopeIntents: AddressIntent[] = [
|
||||||
"list_receivables_counterparties",
|
"list_receivables_counterparties",
|
||||||
"list_open_contracts",
|
"list_open_contracts",
|
||||||
|
|
@ -2154,6 +2164,8 @@ function buildLimitedOffers(input: {
|
||||||
offers.push("показать контрагентов с максимальными хвостами дебиторки по 62/76");
|
offers.push("показать контрагентов с максимальными хвостами дебиторки по 62/76");
|
||||||
} else if (input.intent === "receivables_confirmed_as_of_date") {
|
} else if (input.intent === "receivables_confirmed_as_of_date") {
|
||||||
offers.push("показать подтвержденный реестр открытой дебиторской задолженности на дату среза по 62/76");
|
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") {
|
} else if (input.intent === "vat_payable_confirmed_as_of_date") {
|
||||||
offers.push("показать подтвержденную сумму НДС к уплате на дату среза по счетам 68*");
|
offers.push("показать подтвержденную сумму НДС к уплате на дату среза по счетам 68*");
|
||||||
} else if (input.intent === "vat_liability_confirmed_for_tax_period") {
|
} else if (input.intent === "vat_liability_confirmed_for_tax_period") {
|
||||||
|
|
@ -2208,6 +2220,7 @@ function buildLimitedIntentSignalLine(input: {
|
||||||
bank_operations_by_contract: "Сигнал запроса: нужен срез банковских операций по договору.",
|
bank_operations_by_contract: "Сигнал запроса: нужен срез банковских операций по договору.",
|
||||||
open_items_by_counterparty_or_contract: "Сигнал запроса: нужен контроль незакрытых взаиморасчетов.",
|
open_items_by_counterparty_or_contract: "Сигнал запроса: нужен контроль незакрытых взаиморасчетов.",
|
||||||
list_open_contracts: "Сигнал запроса: нужен список незакрытых договоров на дату.",
|
list_open_contracts: "Сигнал запроса: нужен список незакрытых договоров на дату.",
|
||||||
|
open_contracts_confirmed_as_of_date: "Сигнал запроса: нужен подтвержденный список договоров с открытыми взаиморасчетами на дату.",
|
||||||
list_receivables_counterparties: "Сигнал запроса: нужен ранжированный список должников.",
|
list_receivables_counterparties: "Сигнал запроса: нужен ранжированный список должников.",
|
||||||
list_payables_counterparties: "Сигнал запроса: нужен ранжированный список кредиторов.",
|
list_payables_counterparties: "Сигнал запроса: нужен ранжированный список кредиторов.",
|
||||||
receivables_confirmed_as_of_date: "Сигнал запроса: нужен подтвержденный срез дебиторской задолженности на дату.",
|
receivables_confirmed_as_of_date: "Сигнал запроса: нужен подтвержденный срез дебиторской задолженности на дату.",
|
||||||
|
|
@ -3630,6 +3643,7 @@ export class AddressQueryService {
|
||||||
const allowConfirmedAsOfZeroSnapshot =
|
const allowConfirmedAsOfZeroSnapshot =
|
||||||
filteredRows.length === 0 &&
|
filteredRows.length === 0 &&
|
||||||
(composeIntent === "vat_payable_confirmed_as_of_date" ||
|
(composeIntent === "vat_payable_confirmed_as_of_date" ||
|
||||||
|
composeIntent === "open_contracts_confirmed_as_of_date" ||
|
||||||
composeIntent === "payables_confirmed_as_of_date" ||
|
composeIntent === "payables_confirmed_as_of_date" ||
|
||||||
composeIntent === "receivables_confirmed_as_of_date") &&
|
composeIntent === "receivables_confirmed_as_of_date") &&
|
||||||
(stageStatus === "no_raw_rows" || stageStatus === "materialized_but_filtered_out_by_recipe") &&
|
(stageStatus === "no_raw_rows" || stageStatus === "materialized_but_filtered_out_by_recipe") &&
|
||||||
|
|
|
||||||
|
|
@ -72,6 +72,48 @@ const RECEIVABLES_CONFIRMED_AS_OF_QUERY_TEMPLATE = `
|
||||||
Сумма __ORDER_DIRECTION__
|
Сумма __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 = `
|
const VAT_PAYABLE_CONFIRMED_AS_OF_QUERY_TEMPLATE = `
|
||||||
ВЫБРАТЬ ПЕРВЫЕ __LIMIT__
|
ВЫБРАТЬ ПЕРВЫЕ __LIMIT__
|
||||||
__AS_OF_EXPR__ КАК Период,
|
__AS_OF_EXPR__ КАК Период,
|
||||||
|
|
@ -634,6 +676,17 @@ const BASE_RECIPES: AddressRecipeDefinition[] = [
|
||||||
account_scope_mode: "preferred",
|
account_scope_mode: "preferred",
|
||||||
query_template: "vat_liability_confirmed_tax_period_profile"
|
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",
|
recipe_id: "address_contracts_by_counterparty_v1",
|
||||||
intent: "list_contracts_by_counterparty",
|
intent: "list_contracts_by_counterparty",
|
||||||
|
|
@ -982,6 +1035,7 @@ function maxLimitForIntent(intent: AddressIntent): number {
|
||||||
intent === "contract_usage_and_value" ||
|
intent === "contract_usage_and_value" ||
|
||||||
intent === "vat_payable_forecast" ||
|
intent === "vat_payable_forecast" ||
|
||||||
intent === "vat_liability_confirmed_for_tax_period" ||
|
intent === "vat_liability_confirmed_for_tax_period" ||
|
||||||
|
intent === "open_contracts_confirmed_as_of_date" ||
|
||||||
intent === "list_contracts_by_counterparty" ||
|
intent === "list_contracts_by_counterparty" ||
|
||||||
intent === "list_documents_by_counterparty" ||
|
intent === "list_documents_by_counterparty" ||
|
||||||
intent === "bank_operations_by_counterparty" ||
|
intent === "bank_operations_by_counterparty" ||
|
||||||
|
|
@ -1156,6 +1210,25 @@ export function buildAddressRecipePlan(
|
||||||
})()
|
})()
|
||||||
: recipe.query_template === "contracts_by_counterparty_profile"
|
: recipe.query_template === "contracts_by_counterparty_profile"
|
||||||
? CONTRACTS_BY_COUNTERPARTY_QUERY_TEMPLATE.replaceAll("__LIMIT__", String(resolvedLimit))
|
? 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"
|
: recipe.query_template === "payables_confirmed_as_of_balance_profile"
|
||||||
? (() => {
|
? (() => {
|
||||||
const asOfExpr =
|
const asOfExpr =
|
||||||
|
|
|
||||||
|
|
@ -758,6 +758,40 @@ interface CounterpartyRiskAggregate {
|
||||||
lastPeriod: string | null;
|
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";
|
type PayablesLiabilityCategory = "supplier_or_contractor" | "bank_or_credit" | "tax_or_state" | "other";
|
||||||
|
|
||||||
interface PayablesCounterpartyRiskAggregate extends CounterpartyRiskAggregate {
|
interface PayablesCounterpartyRiskAggregate extends CounterpartyRiskAggregate {
|
||||||
|
|
@ -1553,6 +1587,380 @@ export function contractCandidatesFromRows(rows: ComposeStageRow[]): string[] {
|
||||||
return uniqueStrings(candidates);
|
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(
|
export function composeFactualReply(
|
||||||
intent: AddressIntent,
|
intent: AddressIntent,
|
||||||
rows: ComposeStageRow[],
|
rows: ComposeStageRow[],
|
||||||
|
|
@ -2904,34 +3312,217 @@ export function composeFactualReply(
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (intent === "list_open_contracts") {
|
if (intent === "open_contracts_confirmed_as_of_date") {
|
||||||
const contracts = contractCandidatesFromRows(rows);
|
const asOfDate = resolvePayablesAsOfDate(options);
|
||||||
const counterparties = buildCounterpartyRiskAggregate(rows);
|
const confirmedContracts = buildOpenContractConfirmedBalanceAggregate(rows, asOfDate);
|
||||||
const lines = [
|
const periodFrom = normalizeIsoDateOnly(options.periodFrom);
|
||||||
"Проверил потенциальные разрывы во взаиморасчетах (платежи без закрытия и документы без оплат).",
|
const periodTo = normalizeIsoDateOnly(options.periodTo);
|
||||||
`Строк движения: ${rows.length}.`,
|
const commercialContracts = confirmedContracts.filter((item) => item.category === "commercial");
|
||||||
`Договорных кандидатов: ${contracts.length}.`
|
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) {
|
} else if (counterparties.length > 0) {
|
||||||
lines.push(`Контрагентов с сигналом незакрытых хвостов: ${counterparties.length}.`);
|
lines.push("");
|
||||||
|
lines.push("Блок 4. Контрагенты с сигналом незакрытых расчетов");
|
||||||
|
lines.push(`- Контрагентов с сигналом: ${formatNumberWithDots(counterparties.length)}.`);
|
||||||
lines.push(
|
lines.push(
|
||||||
...counterparties
|
...counterparties
|
||||||
.slice(0, 8)
|
.slice(0, 8)
|
||||||
.map(
|
.map(
|
||||||
(item, index) =>
|
(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 {
|
} else {
|
||||||
lines.push("Договорные якоря в live-строках не выделены; показаны связанные движения как fallback.");
|
lines.push("");
|
||||||
|
lines.push("Блок 4. Позиции не выделены");
|
||||||
|
lines.push("- По текущему live-срезу не удалось выделить договоры с достаточным качеством идентификации.");
|
||||||
|
lines.push("Блок 5. Примеры исходных строк");
|
||||||
lines.push(...formatTopRows(rows, 6));
|
lines.push(...formatTopRows(rows, 6));
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
responseType: "FACTUAL_LIST",
|
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 merged: AddressFilterSet = { ...current };
|
||||||
const reasons: string[] = [];
|
const reasons: string[] = [];
|
||||||
if (!followupContext) {
|
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 };
|
return { filters: merged, reasons };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -470,6 +479,7 @@ function mergeFollowupFilters(
|
||||||
if (
|
if (
|
||||||
intent === "open_items_by_counterparty_or_contract" ||
|
intent === "open_items_by_counterparty_or_contract" ||
|
||||||
intent === "list_open_contracts" ||
|
intent === "list_open_contracts" ||
|
||||||
|
intent === "open_contracts_confirmed_as_of_date" ||
|
||||||
intent === "payables_confirmed_as_of_date" ||
|
intent === "payables_confirmed_as_of_date" ||
|
||||||
intent === "receivables_confirmed_as_of_date" ||
|
intent === "receivables_confirmed_as_of_date" ||
|
||||||
intent === "vat_payable_confirmed_as_of_date"
|
intent === "vat_payable_confirmed_as_of_date"
|
||||||
|
|
@ -550,6 +560,7 @@ function mergeFollowupFilters(
|
||||||
const asOfPrimaryIntent =
|
const asOfPrimaryIntent =
|
||||||
intent === "account_balance_snapshot" ||
|
intent === "account_balance_snapshot" ||
|
||||||
intent === "documents_forming_balance" ||
|
intent === "documents_forming_balance" ||
|
||||||
|
intent === "open_contracts_confirmed_as_of_date" ||
|
||||||
intent === "payables_confirmed_as_of_date" ||
|
intent === "payables_confirmed_as_of_date" ||
|
||||||
intent === "receivables_confirmed_as_of_date" ||
|
intent === "receivables_confirmed_as_of_date" ||
|
||||||
intent === "vat_payable_confirmed_as_of_date";
|
intent === "vat_payable_confirmed_as_of_date";
|
||||||
|
|
@ -587,6 +598,16 @@ function mergeFollowupFilters(
|
||||||
reasons.push("period_from_followup_context");
|
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 };
|
return { filters: merged, reasons };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -594,6 +615,7 @@ function resolveMissingRequiredFilters(intent: AddressIntent, filters: AddressFi
|
||||||
const requiredByIntent: Record<string, Array<keyof AddressFilterSet>> = {
|
const requiredByIntent: Record<string, Array<keyof AddressFilterSet>> = {
|
||||||
account_balance_snapshot: ["account", "as_of_date"],
|
account_balance_snapshot: ["account", "as_of_date"],
|
||||||
documents_forming_balance: ["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"],
|
payables_confirmed_as_of_date: ["as_of_date"],
|
||||||
receivables_confirmed_as_of_date: ["as_of_date"],
|
receivables_confirmed_as_of_date: ["as_of_date"],
|
||||||
vat_payable_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 (
|
if (
|
||||||
intent === "account_balance_snapshot" ||
|
intent === "account_balance_snapshot" ||
|
||||||
intent === "documents_forming_balance" ||
|
intent === "documents_forming_balance" ||
|
||||||
|
intent === "open_contracts_confirmed_as_of_date" ||
|
||||||
intent === "payables_confirmed_as_of_date" ||
|
intent === "payables_confirmed_as_of_date" ||
|
||||||
intent === "receivables_confirmed_as_of_date" ||
|
intent === "receivables_confirmed_as_of_date" ||
|
||||||
intent === "vat_payable_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",
|
"counterparty_activity_lifecycle",
|
||||||
"customer_revenue_and_payments",
|
"customer_revenue_and_payments",
|
||||||
"supplier_payouts_profile",
|
"supplier_payouts_profile",
|
||||||
|
"open_contracts_confirmed_as_of_date",
|
||||||
"list_open_contracts",
|
"list_open_contracts",
|
||||||
"open_items_by_counterparty_or_contract",
|
"open_items_by_counterparty_or_contract",
|
||||||
"list_payables_counterparties",
|
"list_payables_counterparties",
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ export type AddressIntent =
|
||||||
| "vat_payable_forecast"
|
| "vat_payable_forecast"
|
||||||
| "vat_liability_confirmed_for_tax_period"
|
| "vat_liability_confirmed_for_tax_period"
|
||||||
| "vat_payable_confirmed_as_of_date"
|
| "vat_payable_confirmed_as_of_date"
|
||||||
|
| "open_contracts_confirmed_as_of_date"
|
||||||
| "list_contracts_by_counterparty"
|
| "list_contracts_by_counterparty"
|
||||||
| "list_open_contracts"
|
| "list_open_contracts"
|
||||||
| "list_payables_counterparties"
|
| "list_payables_counterparties"
|
||||||
|
|
@ -135,6 +136,7 @@ export interface AddressRecipeDefinition {
|
||||||
| "vat_payable_forecast_profile"
|
| "vat_payable_forecast_profile"
|
||||||
| "vat_liability_confirmed_tax_period_profile"
|
| "vat_liability_confirmed_tax_period_profile"
|
||||||
| "vat_payable_confirmed_as_of_balance_profile"
|
| "vat_payable_confirmed_as_of_balance_profile"
|
||||||
|
| "open_contracts_confirmed_as_of_balance_profile"
|
||||||
| "payables_confirmed_as_of_balance_profile"
|
| "payables_confirmed_as_of_balance_profile"
|
||||||
| "receivables_confirmed_as_of_balance_profile";
|
| "receivables_confirmed_as_of_balance_profile";
|
||||||
required_filters: Array<keyof AddressFilterSet>;
|
required_filters: Array<keyof AddressFilterSet>;
|
||||||
|
|
|
||||||
|
|
@ -177,6 +177,110 @@ describe("address compose stage utf8 headers", () => {
|
||||||
expect(reply.text).toContain("Договор №19/15");
|
expect(reply.text).toContain("Договор №19/15");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("renders explicit heuristic contract-candidates reply for open-contracts intent", () => {
|
||||||
|
const reply = composeFactualReply(
|
||||||
|
"list_open_contracts",
|
||||||
|
[
|
||||||
|
{
|
||||||
|
period: "2020-03-31T23:59:59Z",
|
||||||
|
registrator: "Поступление товаров и услуг 00000000022",
|
||||||
|
account_dt: "60.01",
|
||||||
|
account_kt: "51",
|
||||||
|
amount: 150000,
|
||||||
|
analytics: ["ООО Ромашка", "Договор №19/15"]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
{
|
||||||
|
periodFrom: "2020-03-01",
|
||||||
|
periodTo: "2020-03-31",
|
||||||
|
asOfDate: "2020-03-31",
|
||||||
|
useRubCurrency: true
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(reply.responseType).toBe("FACTUAL_LIST");
|
||||||
|
expect(reply.text).toContain("Итого по предварительному срезу открытых договоров");
|
||||||
|
expect(reply.text).toContain("Блок 1. Статус результата");
|
||||||
|
expect(reply.text).toContain("Результат: предварительный список договоров с возможными незакрытыми расчетами.");
|
||||||
|
expect(reply.text).toContain("Блок 4. Основной список (коммерческие договоры)");
|
||||||
|
expect(reply.semantics?.result_mode).toBe("heuristic_candidates");
|
||||||
|
expect(reply.semantics?.balance_confirmed).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("renders confirmed open-contracts snapshot for exact contract-settlements intent", () => {
|
||||||
|
const reply = composeFactualReply(
|
||||||
|
"open_contracts_confirmed_as_of_date",
|
||||||
|
[
|
||||||
|
{
|
||||||
|
period: "2020-03-31T23:59:59Z",
|
||||||
|
registrator: "Остатки на дату",
|
||||||
|
account_dt: "",
|
||||||
|
account_kt: "60.01",
|
||||||
|
amount: 150000,
|
||||||
|
analytics: ["ООО Ромашка", "Договор №19/15"]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
{
|
||||||
|
periodFrom: "2020-03-01",
|
||||||
|
periodTo: "2020-03-31",
|
||||||
|
asOfDate: "2020-03-31",
|
||||||
|
useRubCurrency: true
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(reply.responseType).toBe("FACTUAL_LIST");
|
||||||
|
expect(reply.text).toContain("Собран подтвержденный срез открытых договоров");
|
||||||
|
expect(reply.text).toContain("Результат: подтвержденный срез договоров с открытыми взаиморасчетами на дату.");
|
||||||
|
expect(reply.text).toContain("Единица ответа: одна строка = один договор, один контрагент и один тип открытого остатка.");
|
||||||
|
expect(reply.text).toContain("Блок 5. Коммерческие договоры с кредиторской задолженностью");
|
||||||
|
expect(reply.semantics?.result_mode).toBe("confirmed_balance");
|
||||||
|
expect(reply.semantics?.balance_confirmed).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("splits confirmed open-contracts output by balance type and hides technical account placeholders", () => {
|
||||||
|
const reply = composeFactualReply(
|
||||||
|
"open_contracts_confirmed_as_of_date",
|
||||||
|
[
|
||||||
|
{
|
||||||
|
period: "2020-03-31T23:59:59Z",
|
||||||
|
registrator: "Остатки на дату",
|
||||||
|
account_dt: "62.01",
|
||||||
|
account_kt: "0",
|
||||||
|
amount: 100000,
|
||||||
|
analytics: ["ООО Ромашка", "Договор №19/15"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
period: "2020-03-31T23:59:59Z",
|
||||||
|
registrator: "Остатки на дату",
|
||||||
|
account_dt: "",
|
||||||
|
account_kt: "60.01",
|
||||||
|
amount: 50000,
|
||||||
|
analytics: ["ООО Ромашка", "Договор №19/15"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
period: "2020-03-31T23:59:59Z",
|
||||||
|
registrator: "Остатки на дату",
|
||||||
|
account_dt: "76.09",
|
||||||
|
account_kt: "",
|
||||||
|
amount: 25000,
|
||||||
|
analytics: ["Комитет госуслуг", "ООО /Альтернатива Плюс/"]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
{
|
||||||
|
periodFrom: "2020-03-01",
|
||||||
|
periodTo: "2020-03-31",
|
||||||
|
asOfDate: "2020-03-31",
|
||||||
|
useRubCurrency: true
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(reply.text).toContain("Блок 4. Коммерческие договоры с дебиторской задолженностью");
|
||||||
|
expect(reply.text).toContain("Блок 5. Коммерческие договоры с кредиторской задолженностью");
|
||||||
|
expect(reply.text).toContain("Блок 8. Финансовые/спорные позиции");
|
||||||
|
expect(reply.text).not.toContain("счета: 62.01; 0");
|
||||||
|
expect(reply.text).toContain("договор не похож на устойчивый договорный реквизит");
|
||||||
|
});
|
||||||
|
|
||||||
it("renders period coverage summary for management profile intent", () => {
|
it("renders period coverage summary for management profile intent", () => {
|
||||||
const reply = composeFactualReply("period_coverage_profile", [
|
const reply = composeFactualReply("period_coverage_profile", [
|
||||||
{
|
{
|
||||||
|
|
@ -1739,7 +1843,7 @@ describe("address intent resolver expansion (M2.3a)", () => {
|
||||||
|
|
||||||
it("resolves unclosed contracts list query without specific anchor", () => {
|
it("resolves unclosed contracts list query without specific anchor", () => {
|
||||||
const result = resolveAddressIntent("Покажи незакрытые договоры на 2020-12-31");
|
const result = resolveAddressIntent("Покажи незакрытые договоры на 2020-12-31");
|
||||||
expect(result.intent).toBe("list_open_contracts");
|
expect(result.intent).toBe("open_contracts_confirmed_as_of_date");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("resolves bank operations by contract for normalized phrase with linked contract wording", () => {
|
it("resolves bank operations by contract for normalized phrase with linked contract wording", () => {
|
||||||
|
|
@ -2923,11 +3027,12 @@ describe("address query limited taxonomy and stage diagnostics", { timeout: 1500
|
||||||
expect(result?.debug.limited_reason_category).not.toBe("unsupported");
|
expect(result?.debug.limited_reason_category).not.toBe("unsupported");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("keeps strict account scope for open-contract scans", async () => {
|
it("keeps strict account scope for confirmed open-contract scans", async () => {
|
||||||
const service = new AddressQueryService();
|
const service = new AddressQueryService();
|
||||||
const result = await service.tryHandle("Какие незакрытые документы по договорам у нас уже давно пора проверить?");
|
const result = await service.tryHandle("Какие незакрытые документы по договорам у нас уже давно пора проверить?");
|
||||||
expect(result?.handled).toBe(true);
|
expect(result?.handled).toBe(true);
|
||||||
expect(result?.debug.detected_intent).toBe("list_open_contracts");
|
expect(result?.debug.detected_intent).toBe("open_contracts_confirmed_as_of_date");
|
||||||
|
expect(result?.debug.selected_recipe).toBe("address_open_contracts_confirmed_as_of_date_v1");
|
||||||
expect(result?.debug.account_scope_mode).toBe("strict");
|
expect(result?.debug.account_scope_mode).toBe("strict");
|
||||||
expect(result?.debug.account_scope_fallback_applied).toBe(false);
|
expect(result?.debug.account_scope_fallback_applied).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
@ -2943,16 +3048,52 @@ describe("address query limited taxonomy and stage diagnostics", { timeout: 1500
|
||||||
expect(result?.debug.limited_reason_category).not.toBe("unsupported");
|
expect(result?.debug.limited_reason_category).not.toBe("unsupported");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("does not return execution_error for open-contracts month query when subconto fields are unavailable", async () => {
|
it("does not return execution_error for confirmed open-contracts month query", async () => {
|
||||||
const service = new AddressQueryService();
|
const service = new AddressQueryService();
|
||||||
const result = await service.tryHandle(
|
const result = await service.tryHandle(
|
||||||
"\u043a\u0430\u043a\u0438\u0435 \u0435\u0441\u0442\u044c \u043e\u0442\u043a\u0440\u044b\u0442\u044b\u0435 \u0434\u043e\u0433\u043e\u0432\u043e\u0440\u0430 \u043d\u0430 \u043c\u0430\u0440\u0442 2020"
|
"\u043a\u0430\u043a\u0438\u0435 \u0435\u0441\u0442\u044c \u043e\u0442\u043a\u0440\u044b\u0442\u044b\u0435 \u0434\u043e\u0433\u043e\u0432\u043e\u0440\u0430 \u043d\u0430 \u043c\u0430\u0440\u0442 2020"
|
||||||
);
|
);
|
||||||
expect(result?.handled).toBe(true);
|
expect(result?.handled).toBe(true);
|
||||||
expect(result?.debug.detected_intent).toBe("list_open_contracts");
|
expect(result?.debug.detected_intent).toBe("open_contracts_confirmed_as_of_date");
|
||||||
expect(result?.debug.limited_reason_category).not.toBe("execution_error");
|
expect(result?.debug.limited_reason_category).not.toBe("execution_error");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("routes direct open-contract month query into exact confirmed mode", async () => {
|
||||||
|
const service = new AddressQueryService();
|
||||||
|
const result = await service.tryHandle("какие есть открытые договора на март 2020");
|
||||||
|
expect(result?.handled).toBe(true);
|
||||||
|
expect(result?.debug.detected_intent).toBe("open_contracts_confirmed_as_of_date");
|
||||||
|
expect(result?.debug.extracted_filters.period_from).toBe("2020-03-01");
|
||||||
|
expect(result?.debug.extracted_filters.period_to).toBe("2020-03-31");
|
||||||
|
expect(result?.debug.extracted_filters.as_of_date).toBe("2020-03-31");
|
||||||
|
expect(result?.debug.route_expectation_status).toBe("matched");
|
||||||
|
expect(result?.debug.route_expectation_reason).toBe("route_expectation_matched");
|
||||||
|
expect(result?.debug.route_expectation_expected_result_modes).toContain("confirmed_balance");
|
||||||
|
expect(result?.debug.requested_result_mode).toBe("confirmed_balance");
|
||||||
|
expect(result?.debug.result_mode).toBe("confirmed_balance");
|
||||||
|
expect(result?.debug.selected_recipe).toBe("address_open_contracts_confirmed_as_of_date_v1");
|
||||||
|
expect(result?.debug.capability_route_mode).toBe("exact");
|
||||||
|
const reply = String(result?.reply_text ?? "");
|
||||||
|
if (result?.response_type !== "LIMITED_WITH_REASON") {
|
||||||
|
expect(reply).toContain("Результат: подтвержденный срез договоров с открытыми взаиморасчетами на дату.");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("keeps preferred account-scope mode for heuristic open-contract fallback recipe and avoids zeroing rows", async () => {
|
||||||
|
const service = new AddressQueryService();
|
||||||
|
const result = await service.tryHandle(
|
||||||
|
"Где у нас есть платежи, но нет документов для закрытия взаиморасчетов? Это уже требует ручной проверки."
|
||||||
|
);
|
||||||
|
expect(result?.handled).toBe(true);
|
||||||
|
expect(result?.debug.detected_intent).toBe("list_open_contracts");
|
||||||
|
if (result?.debug.selected_recipe === "address_open_items_by_party_or_contract_v1") {
|
||||||
|
expect(result?.debug.account_scope_mode).toBe("preferred");
|
||||||
|
expect(result?.debug.account_scope_fallback_applied).toBe(true);
|
||||||
|
expect(result?.debug.rows_after_account_scope).toBeGreaterThan(0);
|
||||||
|
expect(result?.debug.limited_reason_category).not.toBe("empty_match");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
it("routes non-paying counterparties month-risk wording into receivables lane", async () => {
|
it("routes non-paying counterparties month-risk wording into receivables lane", async () => {
|
||||||
const service = new AddressQueryService();
|
const service = new AddressQueryService();
|
||||||
const result = await service.tryHandle(
|
const result = await service.tryHandle(
|
||||||
|
|
@ -3388,7 +3529,7 @@ describe("address query limited taxonomy and stage diagnostics", { timeout: 1500
|
||||||
"\u0441\u043a\u043e\u043a\u0430 \u043d\u0434\u0441\u0430 \u043c\u044b \u0434\u043e\u043b\u0436\u043d\u044b \u043d\u0430 \u0441\u0435\u043d\u0442\u044f\u0431\u0440\u044c 2017",
|
"\u0441\u043a\u043e\u043a\u0430 \u043d\u0434\u0441\u0430 \u043c\u044b \u0434\u043e\u043b\u0436\u043d\u044b \u043d\u0430 \u0441\u0435\u043d\u0442\u044f\u0431\u0440\u044c 2017",
|
||||||
{
|
{
|
||||||
followupContext: {
|
followupContext: {
|
||||||
previous_intent: (seed?.debug.detected_intent as any) ?? "list_open_contracts",
|
previous_intent: (seed?.debug.detected_intent as any) ?? "open_contracts_confirmed_as_of_date",
|
||||||
previous_filters: seed?.debug.extracted_filters,
|
previous_filters: seed?.debug.extracted_filters,
|
||||||
previous_anchor_type: (seed?.debug.anchor_type as any) ?? "unknown",
|
previous_anchor_type: (seed?.debug.anchor_type as any) ?? "unknown",
|
||||||
previous_anchor_value: seed?.debug.anchor_value_resolved ?? seed?.debug.anchor_value_raw ?? null
|
previous_anchor_value: seed?.debug.anchor_value_resolved ?? seed?.debug.anchor_value_raw ?? null
|
||||||
|
|
@ -3743,9 +3884,20 @@ describe("address decompose stage follow-up carryover", () => {
|
||||||
expect(result?.baseReasons).toContain("open_items_from_followup_context");
|
expect(result?.baseReasons).toContain("open_items_from_followup_context");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("derives as-of date from period for open-contract month query", () => {
|
||||||
|
const result = runAddressDecomposeStage("какие есть открытые договора на март 2020", null);
|
||||||
|
expect(result).not.toBeNull();
|
||||||
|
expect(result?.mode.mode).toBe("address_query");
|
||||||
|
expect(result?.intent.intent).toBe("open_contracts_confirmed_as_of_date");
|
||||||
|
expect(result?.filters.extracted_filters.period_from).toBe("2020-03-01");
|
||||||
|
expect(result?.filters.extracted_filters.period_to).toBe("2020-03-31");
|
||||||
|
expect(result?.filters.extracted_filters.as_of_date).toBe("2020-03-31");
|
||||||
|
expect(result?.baseReasons).toContain("as_of_date_derived_from_period_for_open_contracts");
|
||||||
|
});
|
||||||
|
|
||||||
it("keeps VAT debt follow-up in VAT intent even after open-contract context", () => {
|
it("keeps VAT debt follow-up in VAT intent even after open-contract context", () => {
|
||||||
const result = runAddressDecomposeStage("\u0441\u043a\u043e\u043a\u0430 \u043d\u0434\u0441\u0430 \u043c\u044b \u0434\u043e\u043b\u0436\u043d\u044b \u043d\u0430 \u0441\u0435\u043d\u0442\u044f\u0431\u0440\u044c 2017", {
|
const result = runAddressDecomposeStage("\u0441\u043a\u043e\u043a\u0430 \u043d\u0434\u0441\u0430 \u043c\u044b \u0434\u043e\u043b\u0436\u043d\u044b \u043d\u0430 \u0441\u0435\u043d\u0442\u044f\u0431\u0440\u044c 2017", {
|
||||||
previous_intent: "list_open_contracts",
|
previous_intent: "open_contracts_confirmed_as_of_date",
|
||||||
previous_filters: {
|
previous_filters: {
|
||||||
period_from: "2020-03-01",
|
period_from: "2020-03-01",
|
||||||
period_to: "2020-03-31"
|
period_to: "2020-03-31"
|
||||||
|
|
@ -4083,8 +4235,8 @@ describe("address recipe catalog counterparty filtering", () => {
|
||||||
expect(plan.query).toContain("ПРЕДСТАВЛЕНИЕ(БанкПоступление.ДоговорКонтрагента) КАК Договор");
|
expect(plan.query).toContain("ПРЕДСТАВЛЕНИЕ(БанкПоступление.ДоговорКонтрагента) КАК Договор");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("allows extended limit for open-contracts intent", () => {
|
it("allows extended limit for confirmed open-contracts intent", () => {
|
||||||
const selected = selectAddressRecipe("list_open_contracts", {
|
const selected = selectAddressRecipe("open_contracts_confirmed_as_of_date", {
|
||||||
as_of_date: "2020-12-31",
|
as_of_date: "2020-12-31",
|
||||||
limit: 1000
|
limit: 1000
|
||||||
});
|
});
|
||||||
|
|
@ -4097,6 +4249,21 @@ describe("address recipe catalog counterparty filtering", () => {
|
||||||
expect(plan.limit).toBe(1000);
|
expect(plan.limit).toBe(1000);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("builds exact balance query for confirmed open-contracts snapshot", () => {
|
||||||
|
const selected = selectAddressRecipe("open_contracts_confirmed_as_of_date", {
|
||||||
|
as_of_date: "2020-12-31"
|
||||||
|
});
|
||||||
|
expect(selected.selected_recipe?.recipe_id).toBe("address_open_contracts_confirmed_as_of_date_v1");
|
||||||
|
const plan = buildAddressRecipePlan(selected.selected_recipe!, {
|
||||||
|
as_of_date: "2020-12-31"
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(plan.query).toContain("РегистрБухгалтерии.Хозрасчетный.Остатки(");
|
||||||
|
expect(plan.query).toContain("СуммаРазвернутыйОстатокДт");
|
||||||
|
expect(plan.query).toContain("СуммаРазвернутыйОстатокКт");
|
||||||
|
expect(plan.query).toContain("ПОДСТРОКА(ЕСТЬNULL(Остатки.Счет.Код, \"\"), 1, 2) = \"60\"");
|
||||||
|
});
|
||||||
|
|
||||||
it("injects account condition into movements query for account snapshot", () => {
|
it("injects account condition into movements query for account snapshot", () => {
|
||||||
const filters = extractAddressFilters(
|
const filters = extractAddressFilters(
|
||||||
"Какой остаток по счету 60 на дату 2020-07-31",
|
"Какой остаток по счету 60 на дату 2020-07-31",
|
||||||
|
|
|
||||||
|
|
@ -619,7 +619,7 @@ describe("assistant orchestration contract", () => {
|
||||||
expect(decision.orchestrationContract?.unsupported_address_intent_fallback_to_deep).toBe(false);
|
expect(decision.orchestrationContract?.unsupported_address_intent_fallback_to_deep).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("keeps list_open_contracts query in address lane despite 'unclosed' wording", () => {
|
it("keeps confirmed open-contracts query in address lane despite 'unclosed' wording", () => {
|
||||||
const decision = resolveAssistantOrchestrationDecision({
|
const decision = resolveAssistantOrchestrationDecision({
|
||||||
rawUserMessage: "\u041f\u043e\u043a\u0430\u0436\u0438 \u043d\u0435\u0437\u0430\u043a\u0440\u044b\u0442\u044b\u0435 \u0434\u043e\u0433\u043e\u0432\u043e\u0440\u044b \u043d\u0430 2020-12-31",
|
rawUserMessage: "\u041f\u043e\u043a\u0430\u0436\u0438 \u043d\u0435\u0437\u0430\u043a\u0440\u044b\u0442\u044b\u0435 \u0434\u043e\u0433\u043e\u0432\u043e\u0440\u044b \u043d\u0430 2020-12-31",
|
||||||
effectiveAddressUserMessage: "\u041f\u043e\u043a\u0430\u0437\u0430\u0442\u044c \u043d\u0435\u0437\u0430\u043a\u0440\u044b\u0442\u044b\u0435 \u0434\u043e\u0433\u043e\u0432\u043e\u0440\u044b \u043f\u043e \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u044e \u043d\u0430 \u043a\u043e\u043d\u0435\u0446 \u0434\u0435\u043a\u0430\u0431\u0440\u044f 2020 \u0433\u043e\u0434\u0430.",
|
effectiveAddressUserMessage: "\u041f\u043e\u043a\u0430\u0437\u0430\u0442\u044c \u043d\u0435\u0437\u0430\u043a\u0440\u044b\u0442\u044b\u0435 \u0434\u043e\u0433\u043e\u0432\u043e\u0440\u044b \u043f\u043e \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u044e \u043d\u0430 \u043a\u043e\u043d\u0435\u0446 \u0434\u0435\u043a\u0430\u0431\u0440\u044f 2020 \u0433\u043e\u0434\u0430.",
|
||||||
|
|
@ -632,7 +632,7 @@ describe("assistant orchestration contract", () => {
|
||||||
predecomposeContract: {
|
predecomposeContract: {
|
||||||
mode: "address_query",
|
mode: "address_query",
|
||||||
mode_confidence: "high",
|
mode_confidence: "high",
|
||||||
intent: "list_open_contracts",
|
intent: "open_contracts_confirmed_as_of_date",
|
||||||
intent_confidence: "medium"
|
intent_confidence: "medium"
|
||||||
}
|
}
|
||||||
} as any,
|
} as any,
|
||||||
|
|
|
||||||
|
|
@ -3,11 +3,10 @@ import { AddressQueryService } from "../src/services/addressQueryService";
|
||||||
import { resolveAssistantOrchestrationDecision, resolveLivingAssistantModeDecision } from "../src/services/assistantService";
|
import { resolveAssistantOrchestrationDecision, resolveLivingAssistantModeDecision } from "../src/services/assistantService";
|
||||||
|
|
||||||
describe("wave17 run regressions (2026-04-11 real runs)", () => {
|
describe("wave17 run regressions (2026-04-11 real runs)", () => {
|
||||||
it("keeps real run 17:51 data-heavy prompts in address lane", () => {
|
it("keeps real run 17:51 prompts with explicit accounting signals in address lane", () => {
|
||||||
const realRunPrompts = [
|
const realRunPrompts = [
|
||||||
"Где у нас накопились авансы к отгрузкам, которые уже давно пора закрыть или хотя бы перепроверить, чтобы не подозревать худшее?",
|
"\u043a\u0430\u043a\u0438\u0435 \u0443 \u043d\u0430\u0441 \u043d\u0430\u0438\u0431\u043e\u043b\u044c\u0448\u0438\u0435 \u0430\u0432\u0430\u043d\u0441\u044b \u043a \u043f\u043e\u0441\u0442\u0430\u0432\u0449\u0438\u043a\u0430\u043c, \u043a\u043e\u0442\u043e\u0440\u044b\u0435 \u0443\u0436\u0435 \u0434\u0430\u0432\u043d\u043e \u0432\u0438\u0441\u044f\u0442 \u0431\u0435\u0437 \u0437\u0430\u043a\u0440\u044b\u0442\u0438\u044f?",
|
||||||
"Какие контрагенты у нас на этом моменте могут быть причислены к тем, кто вообще не платит уже несколько месяцев?",
|
"\u0432 \u043a\u0430\u043a\u0438\u0445 \u0441\u0434\u0435\u043b\u043a\u0430\u0445 \u043c\u044b \u0432\u0438\u0434\u0438\u043c \u043e\u0442\u0433\u0440\u0443\u0437\u043a\u0438, \u043d\u043e \u0434\u0435\u043d\u044c\u0433\u0438 \u0442\u0430\u043a \u0438 \u043d\u0435 \u043f\u0440\u0438\u0448\u043b\u0438?"
|
||||||
"В каких случаях мы видим зависшие отгрузки, которые уже давно пора закрыть - это грозит проблемами в отчетности."
|
|
||||||
];
|
];
|
||||||
|
|
||||||
for (const prompt of realRunPrompts) {
|
for (const prompt of realRunPrompts) {
|
||||||
|
|
@ -28,8 +27,29 @@ describe("wave17 run regressions (2026-04-11 real runs)", () => {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("keeps vague counterparty risk wording in deep analysis until stronger data anchor appears", () => {
|
||||||
|
const decision = resolveAssistantOrchestrationDecision({
|
||||||
|
rawUserMessage:
|
||||||
|
"\u043a\u0430\u043a\u0438\u0435 \u043a\u043e\u043d\u0442\u0440\u0430\u0433\u0435\u043d\u0442\u044b \u0443 \u043d\u0430\u0441 \u0434\u0430\u0432\u043d\u043e \u043d\u0435 \u043f\u043b\u0430\u0442\u044f\u0442 \u0438 \u044d\u0442\u043e \u0443\u0436\u0435 \u043f\u043e\u0445\u043e\u0436\u0435 \u043d\u0430 \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u0443?",
|
||||||
|
effectiveAddressUserMessage:
|
||||||
|
"\u043a\u0430\u043a\u0438\u0435 \u043a\u043e\u043d\u0442\u0440\u0430\u0433\u0435\u043d\u0442\u044b \u0443 \u043d\u0430\u0441 \u0434\u0430\u0432\u043d\u043e \u043d\u0435 \u043f\u043b\u0430\u0442\u044f\u0442 \u0438 \u044d\u0442\u043e \u0443\u0436\u0435 \u043f\u043e\u0445\u043e\u0436\u0435 \u043d\u0430 \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u0443?",
|
||||||
|
followupContext: null,
|
||||||
|
llmPreDecomposeMeta: null,
|
||||||
|
useMock: false
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(decision.runAddressLane).toBe(false);
|
||||||
|
expect(decision.toolGateReason).toBe("deep_analysis_signal_fallback_to_deep");
|
||||||
|
expect(decision.livingMode).toBe("deep_analysis");
|
||||||
|
expect(decision.livingReason).toBe("deep_analysis_signal_fallback_to_deep");
|
||||||
|
});
|
||||||
|
|
||||||
it("keeps short follow-up style prompts out of chat drift when predecompose says unsupported", () => {
|
it("keeps short follow-up style prompts out of chat drift when predecompose says unsupported", () => {
|
||||||
const shortFollowups = ["без воды?", "и коротко?", "прям сейчас?"];
|
const shortFollowups = [
|
||||||
|
"\u0430 \u0431\u0435\u0437 \u0441\u0432\u043e\u0434\u043a\u0438?",
|
||||||
|
"\u0438 \u043f\u043e \u044d\u0442\u043e\u043c\u0443 \u0442\u043e\u0436\u0435?",
|
||||||
|
"\u043f\u0440\u044f\u043c \u0433\u0434\u0435 \u0436\u0435?"
|
||||||
|
];
|
||||||
|
|
||||||
for (const prompt of shortFollowups) {
|
for (const prompt of shortFollowups) {
|
||||||
const decision = resolveLivingAssistantModeDecision({
|
const decision = resolveLivingAssistantModeDecision({
|
||||||
|
|
@ -47,7 +67,7 @@ describe("wave17 run regressions (2026-04-11 real runs)", () => {
|
||||||
|
|
||||||
it("routes data-scope slang wording to chat mode", () => {
|
it("routes data-scope slang wording to chat mode", () => {
|
||||||
const decision = resolveLivingAssistantModeDecision({
|
const decision = resolveLivingAssistantModeDecision({
|
||||||
userMessage: "по каким конторам можем общаться?",
|
userMessage: "\u0430 \u043a\u0430\u043a\u0430\u044f \u0432\u043e\u043e\u0431\u0449\u0435 \u0431\u0430\u0437\u0430 \u0441\u044e\u0434\u0430 \u043f\u043e\u0434\u0440\u0443\u0431\u043b\u0435\u043d\u0430?",
|
||||||
addressLaneTriggered: false,
|
addressLaneTriggered: false,
|
||||||
useMock: false,
|
useMock: false,
|
||||||
predecomposeMode: "unsupported",
|
predecomposeMode: "unsupported",
|
||||||
|
|
@ -60,11 +80,11 @@ describe("wave17 run regressions (2026-04-11 real runs)", () => {
|
||||||
|
|
||||||
it("keeps open-contracts request in address lane even with stale deep followup context", () => {
|
it("keeps open-contracts request in address lane even with stale deep followup context", () => {
|
||||||
const decision = resolveAssistantOrchestrationDecision({
|
const decision = resolveAssistantOrchestrationDecision({
|
||||||
rawUserMessage: "Покажи незакрытые договоры на 2020-12-31",
|
rawUserMessage: "\u041f\u043e\u043a\u0430\u0436\u0438 \u043d\u0435\u0437\u0430\u043a\u0440\u044b\u0442\u044b\u0435 \u0434\u043e\u0433\u043e\u0432\u043e\u0440\u044b \u043d\u0430 2020-12-31",
|
||||||
effectiveAddressUserMessage: "Покажи незакрытые договоры на 2020-12-31",
|
effectiveAddressUserMessage: "\u041f\u043e\u043a\u0430\u0436\u0438 \u043d\u0435\u0437\u0430\u043a\u0440\u044b\u0442\u044b\u0435 \u0434\u043e\u0433\u043e\u0432\u043e\u0440\u044b \u043d\u0430 2020-12-31",
|
||||||
followupContext: {
|
followupContext: {
|
||||||
previous_question_id: "msg-prev",
|
previous_question_id: "msg-prev",
|
||||||
last_user_message: "почему так по закрытию месяца",
|
last_user_message: "\u043f\u043e\u0447\u0435\u043c\u0443 \u0442\u0430\u043a \u043f\u043e \u0437\u0430\u043a\u0440\u044b\u0442\u0438\u044e \u043c\u0435\u0441\u044f\u0446\u0430",
|
||||||
active_domain: "month_close_costs_20_44",
|
active_domain: "month_close_costs_20_44",
|
||||||
active_requirement_ids: ["R1"],
|
active_requirement_ids: ["R1"],
|
||||||
uncovered_requirement_ids: ["R1"],
|
uncovered_requirement_ids: ["R1"],
|
||||||
|
|
@ -77,7 +97,7 @@ describe("wave17 run regressions (2026-04-11 real runs)", () => {
|
||||||
predecomposeContract: {
|
predecomposeContract: {
|
||||||
mode: "address_query",
|
mode: "address_query",
|
||||||
mode_confidence: "high",
|
mode_confidence: "high",
|
||||||
intent: "list_open_contracts",
|
intent: "open_contracts_confirmed_as_of_date",
|
||||||
intent_confidence: "medium"
|
intent_confidence: "medium"
|
||||||
}
|
}
|
||||||
} as any,
|
} as any,
|
||||||
|
|
@ -90,19 +110,33 @@ describe("wave17 run regressions (2026-04-11 real runs)", () => {
|
||||||
expect(decision.orchestrationContract?.deep_analysis_signal_fallback_to_deep).toBe(false);
|
expect(decision.orchestrationContract?.deep_analysis_signal_fallback_to_deep).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("uses soft unsupported aggregate replies instead of rigid old template", async () => {
|
it("supports strongest aggregate revenue route while keeping unsupported turnover prompt soft", async () => {
|
||||||
const service = new AddressQueryService();
|
const service = new AddressQueryService();
|
||||||
const prompts = ["какой самый доходный год?", "какие обороты по альтернативе за 2020 год"];
|
|
||||||
|
|
||||||
for (const prompt of prompts) {
|
const strongestRevenue = await service.tryHandle(
|
||||||
const result = await service.tryHandle(prompt);
|
"\u043a\u0430\u043a\u043e\u0439 \u0441\u0430\u043c\u044b\u0439 \u0434\u043e\u0445\u043e\u0434\u043d\u044b\u0439 \u0433\u043e\u0434?"
|
||||||
const reply = String(result?.reply_text ?? "");
|
);
|
||||||
|
const strongestReply = String(strongestRevenue?.reply_text ?? "");
|
||||||
|
expect(strongestRevenue?.handled).toBe(true);
|
||||||
|
expect(strongestRevenue?.reply_type).toBe("factual");
|
||||||
|
expect(strongestRevenue?.debug.detected_intent).toBe("customer_revenue_and_payments");
|
||||||
|
expect(strongestRevenue?.debug.selected_recipe).toBe("address_customer_revenue_and_payments_v1");
|
||||||
|
expect(strongestReply).toContain(
|
||||||
|
"\u0422\u043e\u043f-3 \u043b\u0435\u0442 \u043f\u043e \u0441\u0443\u043c\u043c\u0435 \u043f\u043e\u0441\u0442\u0443\u043f\u043b\u0435\u043d\u0438\u0439"
|
||||||
|
);
|
||||||
|
|
||||||
expect(result?.handled).toBe(true);
|
const unsupportedTurnover = await service.tryHandle(
|
||||||
expect(result?.reply_type).toBe("partial_coverage");
|
"\u043a\u0430\u043a\u0438\u0435 \u043e\u0431\u043e\u0440\u043e\u0442\u044b \u043f\u043e \u0430\u043b\u044c\u0442\u0435\u0440\u043d\u0430\u0442\u0438\u0432\u0435 \u0437\u0430 2020 \u0433\u043e\u0434"
|
||||||
expect(result?.debug.limited_reason_category).toBe("unsupported");
|
);
|
||||||
expect(reply).toContain("Что могу сделать сейчас:");
|
const unsupportedReply = String(unsupportedTurnover?.reply_text ?? "");
|
||||||
expect(reply).not.toMatch(/Сейчас этот тип вопроса вне поддерживаемого контура адресного режима/iu);
|
expect(unsupportedTurnover?.handled).toBe(true);
|
||||||
}
|
expect(unsupportedTurnover?.reply_type).toBe("partial_coverage");
|
||||||
|
expect(unsupportedTurnover?.debug.limited_reason_category).toBe("unsupported");
|
||||||
|
expect(unsupportedReply).toContain(
|
||||||
|
"\u0427\u0442\u043e \u043c\u043e\u0433\u0443 \u0441\u0434\u0435\u043b\u0430\u0442\u044c \u0441\u0435\u0439\u0447\u0430\u0441:"
|
||||||
|
);
|
||||||
|
expect(unsupportedReply).not.toContain(
|
||||||
|
"\u0421\u0435\u0439\u0447\u0430\u0441 \u044d\u0442\u043e\u0442 \u0442\u0438\u043f \u0432\u043e\u043f\u0440\u043e\u0441\u0430 \u0432\u043d\u0435 \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u043c\u043e\u0433\u043e \u043a\u043e\u043d\u0442\u0443\u0440\u0430 \u0430\u0434\u0440\u0435\u0441\u043d\u043e\u0433\u043e \u0440\u0435\u0436\u0438\u043c\u0430"
|
||||||
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue