ДОМЕНЫ - ВОПРОСЫ - fix: переведен exact-маршрут кому должны на дату на Хозрасчетный.Остатки (без Движения.СубконтоДт1/Кт1)
This commit is contained in:
@@ -311,12 +311,19 @@ const CONTRACT_USAGE_OVERVIEW_HINTS = [
|
||||
const CUSTOMER_REVENUE_AND_PAYMENTS_HINTS = [
|
||||
"самые доходные клиенты",
|
||||
"самые доходные заказчики",
|
||||
"самые ликвидные клиенты",
|
||||
"самые ликвидные заказчики",
|
||||
"самых ликвидних заказчиков",
|
||||
"топ клиентов по сумме поступлений",
|
||||
"топ заказчиков по сумме поступлений",
|
||||
"кто больше всего принес денег",
|
||||
"кто больше всего принёс денег",
|
||||
"кто принес больше всего денег",
|
||||
"кто принёс больше всего денег",
|
||||
"кто нам больше денег принес",
|
||||
"кто нам больше денег принёс",
|
||||
"кто нам принес больше денег",
|
||||
"кто нам принёс больше денег",
|
||||
"кто нам больше всего занес",
|
||||
"кто нам больше всего занёс",
|
||||
"кто нам принес больше всего",
|
||||
@@ -790,6 +797,19 @@ function hasCustomerRevenueAndPaymentsSignal(text: string): boolean {
|
||||
/(?:кто\s+(?:нам\s+)?(?:больше\s+всего|сам(?:ый|ая|ое|ые)|наибольш(?:ий|ая|ее|ие))\s+(?:прин[её]с|зан[её]с).*(?:деньг|денег))/iu.test(
|
||||
text
|
||||
);
|
||||
const asksWhoBringsMoneyLoose =
|
||||
/(?:кто\s+(?:нам\s+)?(?:больше|больше\s+всех|больше\s+всего).*(?:деньг|денег|доход|выручк).*(?:прин[её]с|зан[её]с))/iu.test(
|
||||
text
|
||||
) ||
|
||||
/(?:кто\s+(?:нам\s+)?(?:прин[её]с|зан[её]с).*(?:больше|больше\s+всех|больше\s+всего).*(?:деньг|денег|доход|выручк))/iu.test(
|
||||
text
|
||||
);
|
||||
const asksLiquidityRanking =
|
||||
/(?:ликвидн|liquid)/iu.test(text) &&
|
||||
(asksCustomerGroup || hasCounterpartyLexeme || /(?:клиент|заказчик|контрагент|customer|client|counterpart)/iu.test(text));
|
||||
const asksProfitableYears =
|
||||
/(?:доходн|выручк|оборот|прибыл|revenue|turnover).*(?:год|года|годы|year|years)/iu.test(text) &&
|
||||
/(?:сам(?:ый|ая|ое|ые)|топ|луч|max|best|наибольш|больше)/iu.test(text);
|
||||
const asksDealBudgetRanking =
|
||||
/(?:сделк|deal|бюджет)/iu.test(text) &&
|
||||
/(?:топ|top|сам(?:ый|ая|ое|ые)|крупн|мален|жирн|мелк|больше\s+всего|чаще\s+всего|наибольш|максимальн|минимальн)/iu.test(
|
||||
@@ -802,7 +822,7 @@ function hasCustomerRevenueAndPaymentsSignal(text: string): boolean {
|
||||
text
|
||||
);
|
||||
const asksValue =
|
||||
/(?:доходн|выручк|приход|поступлен|входящ|зачислен|оплат|плат(?:еж|ёж|ежн|ежей|ежа|ит|ят)|деньг|денег|заработ|оборот|чек|сделк|бюджет|занес|занёс|принес|принёс|revenue|inflow|deal|turnover)/iu.test(
|
||||
/(?:доходн|выручк|приход|поступлен|входящ|зачислен|оплат|плат(?:еж|ёж|ежн|ежей|ежа|ит|ят)|деньг|денег|заработ|оборот|чек|сделк|бюджет|занес|занёс|принес|принёс|ликвидн|revenue|inflow|deal|turnover|liquid)/iu.test(
|
||||
text
|
||||
);
|
||||
const asksRankOrTop = /(?:топ|top|сам(?:ый|ая|ое|ые)|крупн|мален|жирн|мелк|больше\s+всего|чаще\s+всего|наибольш|максимальн)/iu.test(
|
||||
@@ -827,6 +847,15 @@ function hasCustomerRevenueAndPaymentsSignal(text: string): boolean {
|
||||
if (!hasFuzzySupplierLexeme && asksWhoBringsMostMoney) {
|
||||
return true;
|
||||
}
|
||||
if (!hasFuzzySupplierLexeme && asksWhoBringsMoneyLoose) {
|
||||
return true;
|
||||
}
|
||||
if (!hasFuzzySupplierLexeme && asksLiquidityRanking) {
|
||||
return true;
|
||||
}
|
||||
if (!hasFuzzySupplierLexeme && asksProfitableYears) {
|
||||
return true;
|
||||
}
|
||||
if (!hasFuzzySupplierLexeme && (asksRevenueTotal || asksOverallTurnover)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -1073,14 +1073,72 @@ function isMissingSubcontoFieldError(errorText: string | null | undefined): bool
|
||||
if (!normalized) {
|
||||
return false;
|
||||
}
|
||||
return (
|
||||
normalized.includes("поле не найдено") &&
|
||||
(normalized.includes("субконтодт1") ||
|
||||
normalized.includes("subcontodt1") ||
|
||||
normalized.includes("subconto_dt1"))
|
||||
);
|
||||
const ruMissingField = "\u043f\u043e\u043b\u0435 \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d\u043e";
|
||||
const hasMissingFieldSignal = normalized.includes(ruMissingField) || normalized.includes("field not found");
|
||||
if (!hasMissingFieldSignal) {
|
||||
return false;
|
||||
}
|
||||
const hasAnySubcontoSignal =
|
||||
/(?:\u0441\u0443\u0431\u043a\u043e\u043d\u0442\u043e(?:\u0434\u0442|\u043a\u0442)?\d*|subconto(?:_)?(?:dt|kt)?\d*)/iu.test(
|
||||
normalized
|
||||
) ||
|
||||
normalized.includes("subcontodt") ||
|
||||
normalized.includes("subcontokt");
|
||||
return hasAnySubcontoSignal;
|
||||
}
|
||||
|
||||
function buildCompositeSubcontoFallbackQuery(queryText: string): string | null {
|
||||
const source = String(queryText ?? "");
|
||||
if (!source.trim()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const dt1Pattern = /ПРЕДСТАВЛЕНИЕ\(\s*Движения\.СубконтоДт1\s*\)\s+КАК\s+СубконтоДт1\s*,?/iu;
|
||||
const dt2Pattern = /ПРЕДСТАВЛЕНИЕ\(\s*Движения\.СубконтоДт2\s*\)\s+КАК\s+СубконтоДт2\s*,?/iu;
|
||||
const dt3Pattern = /ПРЕДСТАВЛЕНИЕ\(\s*Движения\.СубконтоДт3\s*\)\s+КАК\s+СубконтоДт3\s*,?/iu;
|
||||
const kt1Pattern = /ПРЕДСТАВЛЕНИЕ\(\s*Движения\.СубконтоКт1\s*\)\s+КАК\s+СубконтоКт1\s*,?/iu;
|
||||
const kt2Pattern = /ПРЕДСТАВЛЕНИЕ\(\s*Движения\.СубконтоКт2\s*\)\s+КАК\s+СубконтоКт2\s*,?/iu;
|
||||
const kt3Pattern = /ПРЕДСТАВЛЕНИЕ\(\s*Движения\.СубконтоКт3\s*\)\s+КАК\s+СубконтоКт3\s*,?/iu;
|
||||
|
||||
const lines = source.split(/\r?\n/);
|
||||
let replaced = false;
|
||||
const rewrittenLines = lines.map((line) => {
|
||||
const indent = line.match(/^\s*/)?.[0] ?? "";
|
||||
if (dt1Pattern.test(line)) {
|
||||
replaced = true;
|
||||
return `${indent}ПРЕДСТАВЛЕНИЕ(Движения.СубконтоДт) КАК СубконтоДт1,`;
|
||||
}
|
||||
if (dt2Pattern.test(line)) {
|
||||
replaced = true;
|
||||
return `${indent}"" КАК СубконтоДт2,`;
|
||||
}
|
||||
if (dt3Pattern.test(line)) {
|
||||
replaced = true;
|
||||
return `${indent}"" КАК СубконтоДт3,`;
|
||||
}
|
||||
if (kt1Pattern.test(line)) {
|
||||
replaced = true;
|
||||
return `${indent}ПРЕДСТАВЛЕНИЕ(Движения.СубконтоКт) КАК СубконтоКт1,`;
|
||||
}
|
||||
if (kt2Pattern.test(line)) {
|
||||
replaced = true;
|
||||
return `${indent}"" КАК СубконтоКт2,`;
|
||||
}
|
||||
if (kt3Pattern.test(line)) {
|
||||
replaced = true;
|
||||
return `${indent}"" КАК СубконтоКт3,`;
|
||||
}
|
||||
return line;
|
||||
});
|
||||
|
||||
if (!replaced) {
|
||||
return null;
|
||||
}
|
||||
return rewrittenLines.join("\n");
|
||||
}
|
||||
|
||||
|
||||
|
||||
function applyFutureDatedRowsGuard(
|
||||
rows: NormalizedAddressRow[],
|
||||
intent: AddressIntent,
|
||||
@@ -2158,6 +2216,8 @@ export class AddressQueryService {
|
||||
}
|
||||
|
||||
let effectiveRecipeId = recipeSelection.selected_recipe.recipe_id;
|
||||
let composeIntent: AddressIntent = intent.intent;
|
||||
let routeExpectationIntent: AddressIntent = intent.intent;
|
||||
let plan = enforceStrictAccountScopeForIntent(
|
||||
buildAddressRecipePlan(recipeSelection.selected_recipe, executionFilters),
|
||||
intent.intent
|
||||
@@ -2166,56 +2226,87 @@ export class AddressQueryService {
|
||||
query: plan.query,
|
||||
limit: plan.limit
|
||||
});
|
||||
const allowOpenItemsFallbackForMissingSubconto = intent.intent !== "payables_confirmed_as_of_date";
|
||||
if (
|
||||
mcp.error &&
|
||||
(plan.recipe.recipe_id === "address_movements_receivables_v1" ||
|
||||
plan.recipe.recipe_id === "address_movements_payables_v1") &&
|
||||
isMissingSubcontoFieldError(mcp.error) &&
|
||||
allowOpenItemsFallbackForMissingSubconto
|
||||
) {
|
||||
const fallbackSelection = selectAddressRecipe("open_items_by_counterparty_or_contract", executionFilters);
|
||||
if (fallbackSelection.selected_recipe && fallbackSelection.missing_required_filters.length === 0) {
|
||||
const fallbackPlan = enforceStrictAccountScopeForIntent(
|
||||
buildAddressRecipePlan(fallbackSelection.selected_recipe, executionFilters),
|
||||
intent.intent
|
||||
);
|
||||
const fallbackMcp = await executeAddressMcpQuery({
|
||||
query: fallbackPlan.query,
|
||||
limit: fallbackPlan.limit
|
||||
const missingSubcontoFallbackEligible =
|
||||
plan.recipe.recipe_id === "address_movements_receivables_v1" ||
|
||||
plan.recipe.recipe_id === "address_movements_payables_v1" ||
|
||||
plan.recipe.recipe_id === "address_payables_confirmed_as_of_date_v1";
|
||||
const missingSubcontoErrorDetected = Boolean(
|
||||
mcp.error && missingSubcontoFallbackEligible && isMissingSubcontoFieldError(mcp.error)
|
||||
);
|
||||
if (missingSubcontoErrorDetected) {
|
||||
let missingSubcontoResolvedByComposite = false;
|
||||
const compositeSubcontoQuery = buildCompositeSubcontoFallbackQuery(plan.query);
|
||||
if (compositeSubcontoQuery) {
|
||||
const compositeMcp = await executeAddressMcpQuery({
|
||||
query: compositeSubcontoQuery,
|
||||
limit: plan.limit
|
||||
});
|
||||
if (!fallbackMcp.error) {
|
||||
plan = fallbackPlan;
|
||||
mcp = fallbackMcp;
|
||||
if (intent.intent === "list_payables_counterparties") {
|
||||
if (!compositeMcp.error) {
|
||||
plan = {
|
||||
...plan,
|
||||
query: compositeSubcontoQuery
|
||||
};
|
||||
mcp = compositeMcp;
|
||||
missingSubcontoResolvedByComposite = true;
|
||||
if (!baseReasons.includes("mcp_missing_subconto_axis_auto_fallback_to_composite_subconto")) {
|
||||
baseReasons.push("mcp_missing_subconto_axis_auto_fallback_to_composite_subconto");
|
||||
}
|
||||
if (intent.intent === "payables_confirmed_as_of_date") {
|
||||
if (!baseReasons.includes("confirmed_payables_exact_mode_missing_subconto_axis_fallback_to_composite_subconto")) {
|
||||
baseReasons.push("confirmed_payables_exact_mode_missing_subconto_axis_fallback_to_composite_subconto");
|
||||
}
|
||||
}
|
||||
} else if (!baseReasons.includes("mcp_missing_subconto_axis_auto_fallback_to_composite_subconto_failed")) {
|
||||
baseReasons.push("mcp_missing_subconto_axis_auto_fallback_to_composite_subconto_failed");
|
||||
}
|
||||
} else if (!baseReasons.includes("mcp_missing_subconto_axis_auto_fallback_to_composite_subconto_unavailable")) {
|
||||
baseReasons.push("mcp_missing_subconto_axis_auto_fallback_to_composite_subconto_unavailable");
|
||||
}
|
||||
|
||||
if (!missingSubcontoResolvedByComposite) {
|
||||
const fallbackSelection = selectAddressRecipe("open_items_by_counterparty_or_contract", executionFilters);
|
||||
if (fallbackSelection.selected_recipe && fallbackSelection.missing_required_filters.length === 0) {
|
||||
const fallbackPlan = enforceStrictAccountScopeForIntent(
|
||||
buildAddressRecipePlan(fallbackSelection.selected_recipe, executionFilters),
|
||||
intent.intent
|
||||
);
|
||||
const fallbackMcp = await executeAddressMcpQuery({
|
||||
query: fallbackPlan.query,
|
||||
limit: fallbackPlan.limit
|
||||
});
|
||||
if (!fallbackMcp.error) {
|
||||
plan = fallbackPlan;
|
||||
mcp = fallbackMcp;
|
||||
effectiveRecipeId = fallbackSelection.selected_recipe.recipe_id;
|
||||
}
|
||||
if (!baseReasons.includes("mcp_missing_subconto_field_auto_fallback_to_open_items")) {
|
||||
baseReasons.push("mcp_missing_subconto_field_auto_fallback_to_open_items");
|
||||
}
|
||||
if (
|
||||
intent.intent === "list_payables_counterparties" &&
|
||||
!baseReasons.includes("fallback_recipe_switched_to_open_items")
|
||||
) {
|
||||
baseReasons.push("fallback_recipe_switched_to_open_items");
|
||||
if (!baseReasons.includes("mcp_missing_subconto_field_auto_fallback_to_open_items")) {
|
||||
baseReasons.push("mcp_missing_subconto_field_auto_fallback_to_open_items");
|
||||
}
|
||||
if (!baseReasons.includes("fallback_recipe_switched_to_open_items")) {
|
||||
baseReasons.push("fallback_recipe_switched_to_open_items");
|
||||
}
|
||||
if (intent.intent === "payables_confirmed_as_of_date") {
|
||||
composeIntent = "list_payables_counterparties";
|
||||
routeExpectationIntent = "list_payables_counterparties";
|
||||
if (!baseReasons.includes("confirmed_payables_exact_mode_missing_subconto_fallback_to_open_items")) {
|
||||
baseReasons.push("confirmed_payables_exact_mode_missing_subconto_fallback_to_open_items");
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (!baseReasons.includes("mcp_missing_subconto_field_auto_fallback_failed")) {
|
||||
baseReasons.push("mcp_missing_subconto_field_auto_fallback_failed");
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (!baseReasons.includes("mcp_missing_subconto_field_auto_fallback_failed")) {
|
||||
baseReasons.push("mcp_missing_subconto_field_auto_fallback_failed");
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (!baseReasons.includes("mcp_missing_subconto_field_auto_fallback_unavailable")) {
|
||||
baseReasons.push("mcp_missing_subconto_field_auto_fallback_unavailable");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (
|
||||
mcp.error &&
|
||||
(plan.recipe.recipe_id === "address_movements_receivables_v1" ||
|
||||
plan.recipe.recipe_id === "address_movements_payables_v1") &&
|
||||
missingSubcontoFallbackEligible &&
|
||||
isMissingSubcontoFieldError(mcp.error) &&
|
||||
!allowOpenItemsFallbackForMissingSubconto &&
|
||||
!baseReasons.includes("confirmed_payables_exact_mode_missing_subconto_no_heuristic_fallback")
|
||||
) {
|
||||
baseReasons.push("confirmed_payables_exact_mode_missing_subconto_no_heuristic_fallback");
|
||||
@@ -3035,10 +3126,10 @@ export class AddressQueryService {
|
||||
});
|
||||
}
|
||||
|
||||
const factual = composeFactualReply(intent.intent, filteredRows, composeOptionsFromFilters(executionFilters));
|
||||
const factual = composeFactualReply(composeIntent, filteredRows, composeOptionsFromFilters(executionFilters));
|
||||
const factualResultSemantics = mergeAddressResultSemantics(
|
||||
deriveAddressResultSemantics({
|
||||
intent: intent.intent,
|
||||
intent: composeIntent,
|
||||
selectedRecipe: effectiveRecipeId,
|
||||
filters: filters.extracted_filters,
|
||||
responseType: factual.responseType,
|
||||
@@ -3047,7 +3138,7 @@ export class AddressQueryService {
|
||||
factual.semantics
|
||||
);
|
||||
const finalRouteExpectationAudit = buildRouteExpectationAudit({
|
||||
intent: intent.intent,
|
||||
intent: routeExpectationIntent,
|
||||
selectedRecipe: effectiveRecipeId,
|
||||
requestedResultMode,
|
||||
resultMode: factualResultSemantics.result_mode
|
||||
@@ -3085,7 +3176,7 @@ export class AddressQueryService {
|
||||
routeExpectationAudit: finalRouteExpectationAudit
|
||||
});
|
||||
}
|
||||
if (intent.intent === "payables_confirmed_as_of_date" && factualResultSemantics.balance_confirmed !== true) {
|
||||
if (intent.intent === "payables_confirmed_as_of_date" && composeIntent === "payables_confirmed_as_of_date" && factualResultSemantics.balance_confirmed !== true) {
|
||||
return buildLimitedExecutionResult({
|
||||
mode,
|
||||
shape,
|
||||
|
||||
@@ -26,6 +26,29 @@ __WHERE_CLAUSE__
|
||||
Движения.Период __ORDER_DIRECTION__
|
||||
`;
|
||||
|
||||
const PAYABLES_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
|
||||
И (__PAYABLE_ACCOUNTS_MATCH__)
|
||||
УПОРЯДОЧИТЬ ПО
|
||||
Сумма __ORDER_DIRECTION__
|
||||
`;
|
||||
|
||||
const BANK_DOCS_QUERY_TEMPLATE = `
|
||||
ВЫБРАТЬ ПЕРВЫЕ __LIMIT__
|
||||
БанкСписание.Дата КАК Период,
|
||||
@@ -548,7 +571,8 @@ const BASE_RECIPES: AddressRecipeDefinition[] = [
|
||||
optional_filters: ["period_from", "period_to", "organization", "counterparty", "contract", "limit", "sort"],
|
||||
default_limit: 200,
|
||||
account_scope: ["60", "76"],
|
||||
account_scope_mode: "strict"
|
||||
account_scope_mode: "strict",
|
||||
query_template: "payables_confirmed_as_of_balance_profile"
|
||||
},
|
||||
{
|
||||
recipe_id: "address_movements_receivables_v1",
|
||||
@@ -1001,6 +1025,25 @@ export function buildAddressRecipePlan(
|
||||
.replaceAll("__VAT19_KT_MATCH__", buildAccountPrefixPredicate("Движения.СчетКт", VAT_PAYABLE_19_PREFIXES))
|
||||
: recipe.query_template === "contracts_by_counterparty_profile"
|
||||
? CONTRACTS_BY_COUNTERPARTY_QUERY_TEMPLATE.replaceAll("__LIMIT__", String(resolvedLimit))
|
||||
: recipe.query_template === "payables_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 PAYABLES_CONFIRMED_AS_OF_QUERY_TEMPLATE
|
||||
.replaceAll("__LIMIT__", String(resolvedLimit))
|
||||
.replaceAll("__AS_OF_EXPR__", asOfExpr)
|
||||
.replaceAll("__PAYABLE_ACCOUNTS_MATCH__", buildAccountPrefixPredicate("Остатки.Счет", ["60", "76"]))
|
||||
.replaceAll("__ORDER_DIRECTION__", resolveOrderDirection(filters.sort));
|
||||
})()
|
||||
: MOVEMENTS_QUERY_TEMPLATE
|
||||
.replace("__LIMIT__", String(resolvedLimit))
|
||||
.replace("__WHERE_CLAUSE__", (() => {
|
||||
|
||||
@@ -51,6 +51,7 @@ type CounterpartyProfileFocus =
|
||||
type CounterpartyLifecycleFocus = "active_customers_period" | "active_customers_all_time";
|
||||
type ValueRankingFocus =
|
||||
| "top_by_total"
|
||||
| "top_years_by_total"
|
||||
| "top_by_ops"
|
||||
| "top_by_max_single"
|
||||
| "top_by_avg_check_min_ops"
|
||||
@@ -524,6 +525,13 @@ function detectValueRankingFocus(userMessage: string | null | undefined): ValueR
|
||||
if (!text) {
|
||||
return "top_by_total";
|
||||
}
|
||||
const asksYearlyRevenueRanking =
|
||||
/(?:доходн|выручк|оборот|прибыл|деньг|денег|revenue|turnover|income)/iu.test(text) &&
|
||||
/(?:год|года|годы|year|years|по\s+годам)/iu.test(text) &&
|
||||
/(?:сам(?:ый|ая|ое|ые)|топ|луч|best|max|наибольш|больше)/iu.test(text);
|
||||
if (asksYearlyRevenueRanking) {
|
||||
return "top_years_by_total";
|
||||
}
|
||||
if (/(?:сам(?:ый|ая|ое|ые)\s+высок[а-яё]*|highest|largest)\s+чек|(?:max\s+check|чек\s+макс)/iu.test(text)) {
|
||||
return "top_by_max_single";
|
||||
}
|
||||
@@ -1800,6 +1808,16 @@ export function composeFactualReply(
|
||||
lastPeriod: string | null;
|
||||
}
|
||||
>();
|
||||
const byYear = new Map<
|
||||
number,
|
||||
{
|
||||
year: number;
|
||||
total: number;
|
||||
ops: number;
|
||||
maxSingle: number;
|
||||
counterparties: Set<string>;
|
||||
}
|
||||
>();
|
||||
const deals: Array<{ period: string | null; registrator: string; counterparty: string; amount: number }> = [];
|
||||
|
||||
for (const row of rows) {
|
||||
@@ -1834,10 +1852,31 @@ export function composeFactualReply(
|
||||
counterparty,
|
||||
amount
|
||||
});
|
||||
|
||||
const year = extractYearFromIso(row.period);
|
||||
if (year !== null) {
|
||||
const yearBucket = byYear.get(year);
|
||||
if (!yearBucket) {
|
||||
byYear.set(year, {
|
||||
year,
|
||||
total: amount,
|
||||
ops: 1,
|
||||
maxSingle: amount,
|
||||
counterparties: new Set<string>([counterparty])
|
||||
});
|
||||
} else {
|
||||
yearBucket.total += amount;
|
||||
yearBucket.ops += 1;
|
||||
yearBucket.maxSingle = Math.max(yearBucket.maxSingle, amount);
|
||||
yearBucket.counterparties.add(counterparty);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const profileRows = Array.from(byCounterparty.values());
|
||||
const yearRows = Array.from(byYear.values());
|
||||
const rankedByTotal = [...profileRows].sort((a, b) => b.total - a.total || b.ops - a.ops || a.name.localeCompare(b.name));
|
||||
const rankedByYearTotal = [...yearRows].sort((a, b) => b.total - a.total || b.ops - a.ops || a.year - b.year);
|
||||
const rankedByOps = [...profileRows].sort((a, b) => b.ops - a.ops || b.total - a.total || a.name.localeCompare(b.name));
|
||||
const rankedByMaxSingle = [...profileRows].sort(
|
||||
(a, b) => b.maxSingle - a.maxSingle || b.total - a.total || a.name.localeCompare(b.name)
|
||||
@@ -1877,6 +1916,28 @@ export function composeFactualReply(
|
||||
};
|
||||
}
|
||||
|
||||
if (focus === "top_years_by_total") {
|
||||
const visible = rankedByYearTotal.slice(0, limit);
|
||||
const heading = isSupplier
|
||||
? `Топ-${visible.length} лет по сумме выплат:`
|
||||
: `Топ-${visible.length} лет по сумме поступлений:`;
|
||||
lines.unshift(heading);
|
||||
if (visible.length === 0) {
|
||||
lines.push("По доступному окну не удалось собрать годовые агрегаты по суммам.");
|
||||
} else {
|
||||
lines.push(
|
||||
...visible.map(
|
||||
(item, index) =>
|
||||
`${index + 1}. ${item.year} | сумма: ${item.total} | операций: ${item.ops} | контрагентов: ${item.counterparties.size} | макс: ${item.maxSingle}`
|
||||
)
|
||||
);
|
||||
}
|
||||
return {
|
||||
responseType: "FACTUAL_LIST",
|
||||
text: lines.join("\n")
|
||||
};
|
||||
}
|
||||
|
||||
if (focus === "top_by_ops") {
|
||||
const visible = rankedByOps.slice(0, limit);
|
||||
const heading = isSupplier
|
||||
|
||||
@@ -3664,6 +3664,12 @@ function hasOpenContractsAddressSignal(text) {
|
||||
return hasRequestCue || hasTemporalCue;
|
||||
}
|
||||
const ADDRESS_INTENTS_KEEP_ADDRESS_LANE = new Set([
|
||||
"period_coverage_profile",
|
||||
"document_type_and_account_section_profile",
|
||||
"counterparty_population_and_roles",
|
||||
"counterparty_activity_lifecycle",
|
||||
"customer_revenue_and_payments",
|
||||
"supplier_payouts_profile",
|
||||
"list_open_contracts",
|
||||
"open_items_by_counterparty_or_contract",
|
||||
"payables_confirmed_as_of_date",
|
||||
|
||||
Reference in New Issue
Block a user