NODEDC_1C/llm_normalizer/backend/src/services/addressIntentResolver.ts

2242 lines
130 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import type { AddressIntentResolution } from "../types/addressQuery";
import { resolveCounterpartyAddressIntent } from "./addressCounterpartyIntentSignals";
import { resolveInventoryAddressIntent } from "./addressInventoryIntentSignals";
import {
hasInventoryProfitabilityCue,
hasInventoryPurchaseStem,
hasInventorySaleCue,
hasInventorySupplierCue
} from "./inventoryLifecycleCueHelpers";
const RECEIVABLES_STRONG = [
"кто должен нам",
"кто нам должен",
"кто нам должэн",
"нам должны",
"нам должен",
"нам должэны",
"who owes us",
"receivable",
"receivables",
"debtor",
"debtors",
"дебитор",
"дебиторск"
];
const PAYABLES_STRONG = [
"РєРѕРјСѓ должны РјС",
"РєРѕРјСѓ должэны РјС",
"РєРѕРјСѓ РјС РґРѕР»Р¶РЅС‹",
"РєРѕРјСѓ РјС РґРѕР»Р¶СЌРЅС‹",
"РјС РґРѕР»Р¶РЅС‹",
"РјС РґРѕР»Р¶СЌРЅС‹",
"who we owe",
"payable",
"payables",
"creditor",
"creditors",
"кредитор",
"кредиторск"
];
const ACCOUNT_BALANCE_HINTS = [
"account balance",
"balance by account",
"saldo",
"баланс",
"остаток по счет",
"сальдо по счет",
"по счету",
"что на счете",
"что на счёте",
"на конец"
];
const DOCUMENTS_FORMING_BALANCE_HINTS = [
"documents forming balance",
"docs forming balance",
"documents form balance",
"docs form balance",
"balance documents",
"documents for balance",
"which documents form balance",
"из чего состоит остаток",
"какие документы формируют остаток",
"раскрой остаток по документам",
"документы под остатком"
];
const OPEN_CONTRACTS_HINTS = [
"open contracts",
"unclosed contracts",
"незакрыт",
"не закрыт",
"открыт",
"РґРѕРіРѕРІРѕСЂ",
"контракт"
];
const OPEN_ITEMS_HINTS = [
"open items",
"unclosed items",
"С…РІРѕСЃС‚",
"РІРёСЃСЏС‚",
"незакрыт",
"открыт",
"долг",
"задолж",
"позици"
];
const DOCUMENTS_BY_COUNTERPARTY_HINTS = [
"documents by counterparty",
"docs by counterparty",
"documents by company",
"documents by supplier",
"documents by customer",
"documents by client",
"documents by partner",
"show documents by counterparty",
"list documents by counterparty",
"документы по",
"доступные документы",
"список документов",
"документ",
"РґРѕРєРё",
"РґРѕРєРё РїРѕ",
"РґРѕРє РїРѕ",
"doki",
"docy",
"doci",
"по контрагент"
];
const BANK_OPERATIONS_BY_COUNTERPARTY_HINTS = [
"bank operations by counterparty",
"bank payments by counterparty",
"payment orders by counterparty",
"bank operations by company",
"bank operations by supplier",
"bank operations by customer",
"show bank operations by counterparty",
"bank ops",
"bank oper",
"transactions by counterparty",
"транзак",
"банк",
"банков",
"по банку",
"опер",
"выписк",
"платеж",
"платёж",
"оплат",
"списан",
"списани",
"поступлен",
"поступлени",
"движени"
];
const DOCUMENTS_BY_CONTRACT_HINTS = [
"documents by contract",
"docs by contract",
"show documents by contract",
"list documents by contract",
"документы по договору",
"РґРѕРєРё РїРѕ РґРѕРіРѕРІРѕСЂСѓ",
"РґРѕРє РїРѕ РґРѕРіРѕРІРѕСЂСѓ",
"документы договор",
"РґРѕРіРѕРІРѕСЂ",
"документы по контракту",
"доки по контракту",
"контракт"
];
const BANK_OPERATIONS_BY_CONTRACT_HINTS = [
"bank operations by contract",
"bank payments by contract",
"payment orders by contract",
"transactions by contract",
"bank ops by contract",
"банковские операции по договору",
"платежи по договору",
"выписка по договору",
"банковские операции по контракту",
"платежи по контракту",
"выписка по контракту"
];
const BANK_OPERATION_CORE_HINTS = [
"банк",
"банков",
"операц",
"опер",
"выписк",
"платеж",
"платёж",
"оплат",
"списан",
"поступлен",
"движени",
"транзак",
"bank",
"payment",
"payments",
"transaction",
"transactions",
"statement",
"wire"
];
const PERIOD_COVERAGE_PROFILE_HINTS = [
"за какие годы",
"за какие года",
"в базе есть данные",
"покрытие периодов",
"диапазон лет",
"профиль данных",
"самый активный год",
"самый активный месяц",
"самый пассивный год",
"самый пассивный месяц",
"наименее активный год",
"наименее активный месяц",
"минимум документов по году",
"минимум операций по месяцу",
"год с минимальным количеством документов",
"месяц с минимальным количеством операций",
"активный год по количеству документов",
"активный месяц по количеству операций",
"most active year",
"most active month",
"least active year",
"least active month",
"year coverage",
"data coverage"
];
const DOCUMENT_TYPE_AND_ACCOUNT_SECTION_PROFILE_HINTS = [
"типы документов",
"типы доков",
"документы чаще всего",
"документы реже всего",
"редкие типы документов",
"наименее используемые типы документов",
"частые типы документов",
"сводка по типам документов",
"доля типов документов",
"разделы учета",
"разделы учёта",
"наиболее заполнены",
"наименее заполнены",
"почти не используются",
"account section",
"document types usage",
"document type profile"
];
const COUNTERPARTY_POPULATION_AND_ROLES_HINTS = [
"сколько всего контрагентов",
"сколько уникальных контрагентов",
"сколько контрагентов в базе",
"сколько заказчиков",
"сколько поставщиков",
"сколько клиентов",
"сколько покупателей",
"скока всего контрагентов",
"скока уникальных контрагентов",
"скока контрагентов в базе",
"скока заказчиков",
"скока поставщиков",
"скока клиентов",
"скока покупателей",
"скок контрагентов",
"скок контрагентов в базе",
"скок заказчиков",
"скок поставщиков",
"скок клиентов",
"скок покупателей",
"сколько смешанных контрагентов",
"типы контрагентов",
"разбей контрагентов",
"раздели контрагентов",
"counterparty population",
"counterparty roles",
"customer supplier split"
];
const COUNTERPARTY_ACTIVITY_LIFECYCLE_HINTS = [
"какие заказчики работали",
"какие заказчики активны",
"какие клиенты работали",
"какие клиенты активны",
"какие контрагенты работали",
"какие поставщики работали",
"список заказчиков",
"список клиентов",
"список заказчиков за все время",
"список клиентов за все время",
"список активных заказчиков",
"список активных клиентов",
"новые заказчики",
"новые клиенты",
"новые контрагенты",
"впервые в",
"кто исчез",
"кто ушел",
"кто ушёл",
"только один раз",
"дольше всего",
"дольше всех",
"долгоживущие контрагенты",
"регулярные поставщики",
"эпизодические поставщики",
"давно не использовались поставщики",
"всех заков",
"кто был активен",
"потом отвалился",
"ровно один раз",
"и пропал",
"самые старые по сотрудничеству",
"разбей поставщиков на регуляр и разовые",
"кто новые в этом году",
"active customers",
"сколько лет активности в базе",
"сколько лет активности в 1с",
"сколько лет в базе 1с",
"какой первый платеж",
"какое первое поступление",
"когда была первая активность",
"когда была последняя активность",
"первая активность в базе",
"последняя активность в базе",
"customer activity list",
"counterparty lifecycle"
];
const CONTRACT_USAGE_OVERVIEW_HINTS = [
"сколько всего договоров",
"сколько договоров заведено",
"сколько договоров в базе",
"сколько договоров использовались",
"сколько договоров использовалось",
"РґРѕРіРѕРІРѕСЂС‹ total vs used",
"обзор договорной базы",
"договорная база total used",
"неиспользуемые договоры",
"давно не использовались договоры",
"мертвые договоры",
"мёртвые договоры",
"stale contracts",
"unused contracts",
"contracts total used",
"contract usage overview"
];
const CUSTOMER_REVENUE_AND_PAYMENTS_HINTS = [
"самые доходные клиенты",
"самые доходные заказчики",
"самые ликвидные клиенты",
"самые ликвидные заказчики",
"самых ликвидних заказчиков",
"топ клиентов по сумме поступлений",
"топ заказчиков по сумме поступлений",
"кто больше всего принес денег",
"кто больше всего принёс денег",
"кто принес больше всего денег",
"кто принёс больше всего денег",
"кто нам больше денег принес",
"кто нам больше денег принёс",
"кто нам принес больше денег",
"кто нам принёс больше денег",
"кто нам больше всего занес",
"кто нам больше всего занёс",
"кто нам принес больше всего",
"кто нам принёс больше всего",
"кто платит чаще всего",
"средний чек клиентов",
"средний чек заказчиков",
"крупные сделки по поступлениям",
"маленькие сделки по поступлениям",
"smallest deals by inflow",
"largest deals by inflow",
"top customers by inflow",
"top customers by revenue"
];
const SUPPLIER_PAYOUTS_PROFILE_HINTS = [
"топ поставщиков по сумме выплат",
"РєРѕРјСѓ РјС Р±РѕР»СЊС€Рµ всего заплатили",
"кому ушло больше всего денег",
"РєРѕРјСѓ РјС Р±РѕР»СЊС€Рµ всего сгрузили денег",
"поставщики по выплатам",
"поставщики по исходящим платежам",
"поставщики с максимальным числом выплат",
"крупные разовые выплаты поставщикам",
"top suppliers by payouts",
"top suppliers by outgoing payments"
];
const CONTRACT_USAGE_AND_VALUE_HINTS = [
"договоры по обороту",
"договоры по сумме оборота",
"топ договоров по обороту",
"контракты по обороту",
"контракты по сумме оборота",
"топ контрактов по обороту",
"договоры с минимальным бюджетом",
"договоры с самым маленьким бюджетом",
"контракты с минимальным бюджетом",
"контракты с самым маленьким бюджетом",
"активные договоры по бюджету",
"активные контракты по бюджету",
"контрагенты с несколькими договорами",
"несколько договоров у контрагента",
"мультидоговорные контрагенты",
"какие договоры активны",
"какие контракты активны",
"рабочие договоры",
"рабочие контракты",
"contracts by turnover",
"contracts by budget"
];
const CONTRACT_LIST_BY_COUNTERPARTY_HINTS = [
"РґРѕРіРѕРІРѕСЂС‹ РїРѕ",
"РґРѕРіРѕРІРѕСЂР° РїРѕ",
"СЃРїРёСЃРѕРє РґРѕРіРѕРІРѕСЂРѕРІ РїРѕ",
"покажи договоры по",
"выведи договоры по",
"контракты по",
"список контрактов по",
"покажи контракты по",
"выведи контракты по",
"contracts by counterparty",
"list contracts by counterparty",
"show contracts by counterparty"
];
function hasAny(text: string, patterns: string[]): boolean {
return patterns.some((item) => text.includes(item));
}
function hasFlexibleReceivablesDebtSignal(text: string): boolean {
const normalized = String(text ?? "");
if (!normalized) {
return false;
}
const hasFlexibleWhoOwesUs =
/(?:\u043a\u0442\u043e(?:\s+\S+){0,4}\s+\u043d\u0430\u043c(?:\s+\S+){0,4}\s+\u0434\u043e\u043b\u0436)/iu.test(normalized) ||
/(?:\u043d\u0430\u043c(?:\s+\S+){0,4}\s+\u043a\u0442\u043e(?:\s+\S+){0,4}\s+\u0434\u043e\u043b\u0436)/iu.test(normalized);
const hasTorchatToUsSignal =
/(?:\u043d\u0430\u043c(?:\s+\S+){0,3}\s+\u0442\u043e\u0440\u0447(?:\u0430\u0442|\u0438\u0442)|\u0442\u043e\u0440\u0447(?:\u0430\u0442|\u0438\u0442)(?:\s+\S+){0,3}\s+\u043d\u0430\u043c)/iu.test(
normalized
);
return hasFlexibleWhoOwesUs || hasTorchatToUsSignal;
}
function hasFlexiblePayablesDebtSignal(text: string): boolean {
const normalized = String(text ?? "");
if (!normalized) {
return false;
}
return (
/(?:РєРѕРјСѓ(?:\s+\S+){0,4}\s+РјС(?:\s+\S+){0,4}\s+РґРѕР»Р)/iu.test(normalized) ||
/(?:РјС(?:\s+\S+){0,4}\s+РєРѕРјСѓ(?:\s+\S+){0,4}\s+РґРѕР»Р)/iu.test(normalized)
);
}
function tokenizeText(text: string): string[] {
return String(text ?? "")
.toLowerCase()
.split(/[^a-zР°-СЏС0-9]+/iu)
.map((token) => token.trim())
.filter((token) => token.length > 0);
}
function trimRussianEnding(token: string): string {
return token.replace(
/(?:РёСЏРјРё|СЏРјРё|ами|РѕРіРѕ|ему|РѕРјСѓ|СРјРё|РёРјРё|РёРµР|РµР|РёР|СР|РѕР|СЏС|Р°С|РѕРІ|ев|ам|СЏРј|РѕРј|ем|С|Рё|Р°|СЏ|Сѓ|СЋ|Рµ|Рѕ)$/u,
""
);
}
function normalizeLexemeToken(rawToken: string): string {
const token = String(rawToken ?? "").toLowerCase().replace(/[^a-zР°-СЏС0-9]+/gu, "");
if (!token) {
return "";
}
if (/^[a-z0-9]+$/u.test(token)) {
return token;
}
return trimRussianEnding(token);
}
function levenshteinDistance(a: string, b: string): number {
if (a === b) {
return 0;
}
if (!a.length) {
return b.length;
}
if (!b.length) {
return a.length;
}
const prev = new Array<number>(b.length + 1);
const curr = new Array<number>(b.length + 1);
for (let j = 0; j <= b.length; j += 1) {
prev[j] = j;
}
for (let i = 1; i <= a.length; i += 1) {
curr[0] = i;
for (let j = 1; j <= b.length; j += 1) {
const cost = a[i - 1] === b[j - 1] ? 0 : 1;
curr[j] = Math.min(
prev[j] + 1,
curr[j - 1] + 1,
prev[j - 1] + cost
);
}
for (let j = 0; j <= b.length; j += 1) {
prev[j] = curr[j];
}
}
return prev[b.length];
}
function hasFuzzyLexeme(text: string, lexemeRoots: string[]): boolean {
const normalizedRoots = lexemeRoots
.map((root) => normalizeLexemeToken(root))
.filter((root) => root.length > 0);
if (normalizedRoots.length === 0) {
return false;
}
const tokens = tokenizeText(text)
.map((token) => normalizeLexemeToken(token))
.filter((token) => token.length >= 4);
for (const token of tokens) {
for (const root of normalizedRoots) {
if (token.includes(root)) {
return true;
}
if (root.includes(token) && token.length >= 5) {
return true;
}
const maxDistance = root.length >= 7 ? 2 : 1;
if (Math.abs(token.length - root.length) > maxDistance) {
continue;
}
if (levenshteinDistance(token, root) <= maxDistance) {
return true;
}
}
}
return false;
}
export function hasCompactAccountCodeToken(text: string): boolean {
// Match compact account tokens while reducing false positives on short-year literals like "22 РіРѕРґ".
const source = String(text ?? "");
if (!source) {
return false;
}
// Safe compact form: 60.01 / 62.1
if (/(?<![\d-])\d{2}[.,]\d{1,2}(?![\d-])/u.test(source)) {
return true;
}
// Plain two-digit code is accepted only in explicit account context.
if (/(?:СЃС[РµС]С|account)\D{0,12}\d{2}(?![\d-])/iu.test(source)) {
return true;
}
if (/(?:^|\s)РїРѕ\s+\d{2}(?=$|[\s,.;:!?])/iu.test(source)) {
if (!/(?:^|\s)(?:Р·Р°|РІ)\s+\d{2}\s*(?:Рі(?:РѕРґ|РѕРґР°)?|year)\b/iu.test(source)) {
return true;
}
}
return false;
}
function hasDocumentsFormingBalanceSignal(text: string): boolean {
if (hasAny(text, DOCUMENTS_FORMING_BALANCE_HINTS)) {
return true;
}
const hasLooseAccountCodeToken = hasCompactAccountCodeToken(text);
const hasDocLexeme = /(?:докуменС|РґРѕРє(?:Рё|ам|Р°С|РѕРІ|Р°)?)/u.test(text);
const hasFormingLexeme = text.includes("формир");
const hasBalanceLexeme = text.includes("остат");
const hasAccountLexeme =
text.includes("счет") || text.includes("счёт") || hasAccountNumberAnchor(text) || hasLooseAccountCodeToken;
if (hasDocLexeme && hasFormingLexeme && hasBalanceLexeme && hasAccountLexeme) {
return true;
}
if (
hasDocLexeme &&
hasBalanceLexeme &&
hasAccountLexeme &&
(text.includes("раскрой") || text.includes("раскид") || text.includes("под остатк"))
) {
return true;
}
if (hasBalanceLexeme && hasAccountLexeme && text.includes("из чего состоит")) {
return true;
}
return hasBalanceLexeme && hasAccountLexeme && /РёР·\s+Сего\s+РѕСЃСР°С/u.test(text);
}
function hasDocumentsFormingBalanceAccountAnchor(text: string): boolean {
if (hasAccountNumberAnchor(text) || text.includes("счет") || text.includes("счёт")) {
return true;
}
// Allow compact account mentions like "60.01" in slang prompts without explicit "счет".
return hasCompactAccountCodeToken(text);
}
function hasAccountBalanceSignal(text: string): boolean {
if (hasAny(text, ACCOUNT_BALANCE_HINTS)) {
return true;
}
const hasAccountLexeme =
hasAccountNumberAnchor(text) || hasCompactAccountCodeToken(text) || /(?:^|\s)РїРѕ\s+\d{2}(?:[.,]\d{1,2})?(?=$|[\s,.;:!?])/u.test(text);
const hasBalanceLexeme =
text.includes("баланс") ||
text.includes("остат") ||
text.includes("сальд") ||
text.includes("saldo") ||
text.includes("balance") ||
text.includes("СЃРєРѕРєР°") ||
text.includes("сколько") ||
/РЅР°\s+РєРѕРЅРµС/u.test(text);
if (hasAccountLexeme && hasBalanceLexeme) {
return true;
}
const hasAsOfStyleDate =
/\b(19|20)\d{2}[./-](0?[1-9]|1[0-2])(?:[./-](0?[1-9]|[12]\d|3[01]))\b/u.test(text) ||
/(?:РЅР°\s+ССѓ\s+РРµ\s+РґР°С[ауеС]|same\s+date|the\s+same\s+date)/iu.test(text);
const hasFollowupBalanceVerb = /(?:вернись|вернуССЊСЃСЏ|вернуССЊ|back|return)/iu.test(text);
return hasAccountLexeme && hasAsOfStyleDate && hasFollowupBalanceVerb;
}
function hasForecastTaxSignal(text: string): boolean {
const hasForecastLexeme =
/(?:РїСЂРѕРіРЅРѕР·|forecast|план(?:\s+плаСРµРР°|\s+оплаСС)?|РїСЂРёРєРёРЅ(?:СѓССЊ|ем|Сѓ|СЊ|СѓР»|ули|СѓСЃСЊ|СѓСЃСЊ))/iu.test(text);
const hasTaxLexeme = /(?:ндс|vat|налог)/iu.test(text);
return hasForecastLexeme && hasTaxLexeme;
}
function hasVatLiabilityConfirmedTaxPeriodSignal(text: string): boolean {
const hasVatLexeme = /(?:РЅРґСЃ|vat)/iu.test(text);
if (!hasVatLexeme) {
return false;
}
const hasPaymentCue =
/(?:Рє\s+уплаСРµ|надо|РЅСѓРРЅРѕ|заплаСРёССЊ|уплаСРёССЊ|плаС[РµС]Р|плаСРµРРєСѓ|РІ\s+налогов|РІ\s+Р±СЋРґРРµС|РґРѕР»РРЅ[Р°С]?\s+заплаСРёССЊ|РјС\s+РґРѕР»РРЅ[Р°С]?|РґРѕР»РРЅ[Р°С]?\s+РјС|СЃРіСЂСѓР·(?:РёССЊ|РёРј|РёССЊ|РёСРµ|РёР»|ила|или|РєР°))/iu.test(
text
);
if (!hasPaymentCue) {
return false;
}
const hasAsOfCue = /(?:РЅР°\s+РґР°С|РїРѕ\s+СЃРѕСЃСРѕСЏРЅРёСЋ|РЅР°\s+РєРѕРЅРµС|as\s+of)/iu.test(text);
if (hasAsOfCue) {
return false;
}
const hasTaxAuthorityCue = /(?:РІ\s+налогов|РІ\s+Р±СЋРґРРµС|деклараС|налогов(?:СР|СѓСЋ)\s+период)/iu.test(text);
const hasQuarterCue = /(?:\b[1-4]\s*(?:кварСал|РєРІ\.?)\b|кварСал|РєРІ\.?)/iu.test(text);
const hasZaPeriodCue =
/(?:Р·Р°\s+(?:\d{4}|январ|Севрал|марС|апрел|РјР°Р|РёСЋРЅ|РёСЋР»|авгусС|сенССЏР±СЂ|РѕРєССЏР±СЂ|РЅРѕСЏР±СЂ|декабр|кварСал|РєРІ\.?|месяС|РіРѕРґ|период))/iu.test(
text
);
const hasExplicitDayDate =
/\b(?:\d{1,2}[./-]\d{1,2}[./-](?:\d{2}|\d{4})|(?:19|20)\d{2}[./-]\d{1,2}[./-]\d{1,2})\b/u.test(text);
const hasMonthYearNaCue =
/(?:РЅР°\s+(?:январ|Севрал|марС|апрел|РјР°Р|РёСЋРЅ|РёСЋР»|авгусС|сенССЏР±СЂ|РѕРєССЏР±СЂ|РЅРѕСЏР±СЂ|декабр)\S*\s+(?:19|20)\d{2})/iu.test(
text
);
const hasHowMuchCue = /(?:сколько|скока|скок)/iu.test(text);
// "На март 2020" и конкретная дата без налогового контекста чаще означают as-of срез.
if (!hasTaxAuthorityCue && !hasZaPeriodCue && !hasQuarterCue && (hasMonthYearNaCue || hasExplicitDayDate)) {
return false;
}
return hasTaxAuthorityCue || hasZaPeriodCue || hasQuarterCue || (hasHowMuchCue && hasTaxAuthorityCue);
}
function hasVatPayableConfirmedSignal(text: string): boolean {
const hasVatLexeme = /(?:РЅРґСЃ|vat)/iu.test(text);
if (!hasVatLexeme) {
return false;
}
const hasPaymentCue =
/(?:Рє\s+уплаСРµ|надо|РЅСѓРРЅРѕ|заплаСРёССЊ|уплаСРёССЊ|плаС[РµС]Р|плаСРµРРєСѓ|РІ\s+налогов|РІ\s+Р±СЋРґРРµС|РґРѕР»РРЅ[Р°С]?\s+заплаСРёССЊ|РјС\s+РґРѕР»РРЅ[Р°С]?|РґРѕР»РРЅ[Р°С]?\s+РјС|СЃРіСЂСѓР·(?:РёССЊ|РёРј|РёССЊ|РёСРµ|РёР»|ила|или|РєР°))/iu.test(
text
);
if (!hasPaymentCue) {
return false;
}
const hasDateOrPeriodCue =
/(?:РЅР°\s+РґР°С|РїРѕ\s+СЃРѕСЃСРѕСЏРЅРёСЋ|РЅР°\s+РєРѕРЅРµС|Р·Р°\s+(?:\d{4}|январ|Севрал|марС|апрел|РјР°Р|РёСЋРЅ|РёСЋР»|авгусС|сенССЏР±СЂ|РѕРєССЏР±СЂ|РЅРѕСЏР±СЂ|декабр)|РЅР°\s+(?:январ|Севрал|марС|апрел|РјР°Р|РёСЋРЅ|РёСЋР»|авгусС|сенССЏР±СЂ|РѕРєССЏР±СЂ|РЅРѕСЏР±СЂ|декабр)\S*(?:\s+(?:19|20)\d{2})?|кварСал|месяС|РіРѕРґ|период|\b\d{4}[./-]\d{2}[./-]\d{2}\b)/iu.test(
text
);
return hasDateOrPeriodCue || /(?:сколько|скока|скок)/iu.test(text);
}
function hasPeriodCoverageProfileSignal(text: string): boolean {
if (hasAny(text, PERIOD_COVERAGE_PROFILE_HINTS)) {
return true;
}
if (/(?:Р·Р°\s+какие\s+РіРѕРґ[Р°-СЏС]*).*(?:баз[Р°С].*РРёРІ|период|данн)/iu.test(text)) {
return true;
}
if (/(?:какоР\s+РіРѕРґ[Р°-СЏС]*).*(?:РїРѕ\s+РґРѕРє|докам|докуменСам)/iu.test(text)) {
return true;
}
if (/(?:какоР\s+месяС[Р°-СЏС]*).*(?:РїРёРє|РїРѕ\s+операС)/iu.test(text)) {
return true;
}
if (/(?:месяС[\s-]*РїРёРє).*(?:операС|ops?|operation)/iu.test(text)) {
return true;
}
if (/(?:top\s*year|top\s*month|years?\/top\s*year|years?\s*top\s*year)/iu.test(text)) {
return true;
}
if (/(?:Р·Р°\s+какие\s+РіРѕРґ[Р°-СЏС]*\s+РІ\s+баз[РµС]\s+есССЊ\s+данн)/iu.test(text)) {
return true;
}
if (
/(?:какоР\s+РіРѕРґ[Р°-СЏС]*\s+сам(?:СР|ая|РѕРµ)\s+(?:акСРёРІ|пассив)|какоР\s+РіРѕРґ[Р°-СЏС]*\s+наименее\s+акСРёРІ|РіРѕРґ\s+СЃ\s+минимальн)/iu.test(text) &&
/(?:докуменС|doc)/iu.test(text)
) {
return true;
}
if (
/(?:какоР\s+месяС[Р°-СЏС]*\s+сам(?:СР|ая|РѕРµ)\s+(?:акСРёРІ|пассив)|какоР\s+месяС[Р°-СЏС]*\s+наименее\s+акСРёРІ|месяС\s+СЃ\s+минимальн)/iu.test(text) &&
/(?:операС|operation|ops?)/iu.test(text)
) {
return true;
}
if (/(?:РїСЂРѕСРёР»[СЊСЏ]\s+данн|РїРѕРєСЂСС(?:РёРµ|РёСЏ)\s+период|диапазон\s+леС)/iu.test(text)) {
return true;
}
return false;
}
function hasDocumentTypeAndAccountSectionProfileSignal(text: string): boolean {
if (hasAny(text, DOCUMENT_TYPE_AND_ACCOUNT_SECTION_PROFILE_HINTS)) {
return true;
}
if (/(?:какиС?\s+РґРѕРє(?:РѕРІ|Рё)?).*(?:больСРµ\s+всего|СР°СРµ\s+всего|РєСЂСѓС)/iu.test(text)) {
return true;
}
if (/(?:СЃРІРѕРґРє[ауи].*СРёРї[Р°-СЏС]*\s+РґРѕРє(?:уменС|РѕРІ|Рё)?).*(?:РґРѕР»[СЊСЏ]|объем|РѕР±СЉ[РµС]Рј)/iu.test(text)) {
return true;
}
if (/(?:какие\s+СРёРї[Р°С]\s+РґРѕРє(?:уменС|РѕРІ|Рё)?\s+(?:использ|СР°СРµ|больС))/iu.test(text)) {
return true;
}
if (/(?:какие\s+СРёРї[Р°С]\s+РґРѕРє(?:уменС|РѕРІ|Рё)?\s+(?:СЂРµРРµ|редк|наименее|РјРёРЅРёРј))/iu.test(text)) {
return true;
}
if (/(?:СРёРїС?\s+РґРѕРє(?:уменС|РѕРІ|Рё)?\s+Рё\s+РёС\s+РґРѕР»[СЊСЏ])/iu.test(text)) {
return true;
}
if (/(?:какие\s+раздел[СР°]\s+СѓС[РµС]СР°\s+(?:наибол|наимен|заполн|РїРѕССРё\s+РЅРµ))/iu.test(text)) {
return true;
}
if (/(?:раздел[СР°]\s+СѓС[РµС]СР°).*(?:РРёСЂРЅ|мерСРІ|РїСѓСЃС|использ)/iu.test(text)) {
return true;
}
return false;
}
function hasCounterpartyPopulationAndRolesSignal(text: string): boolean {
if (hasLifecycleSegmentationSignal(text)) {
return false;
}
if (hasAny(text, COUNTERPARTY_POPULATION_AND_ROLES_HINTS)) {
return true;
}
if (
/(?:(?:сколько|СЃРєРѕРєР°|СЃРєРѕРє)\s+(?:всего\s+)?уникальн(?:СС|СРµ|РѕРіРѕ)?\s+РєРѕРЅСрагенС|(?:сколько|СЃРєРѕРєР°|СЃРєРѕРє)\s+(?:всего\s+)?РєРѕРЅСрагенС(?:РѕРІ|Р°)?(?:\s+РІ\s+баз[РµС])?)/iu.test(
text
)
) {
return true;
}
if (
/(?:(?:сколько|СЃРєРѕРєР°|СЃРєРѕРє)\s+(?:Сѓ\s+нас\s+)?заказСРёРє(?:РѕРІ|Р°)?|(?:сколько|СЃРєРѕРєР°|СЃРєРѕРє)\s+(?:Сѓ\s+нас\s+)?РїРѕСЃСавСРёРє(?:РѕРІ|Р°)?|(?:сколько|СЃРєРѕРєР°|СЃРєРѕРє)\s+(?:Сѓ\s+нас\s+)?клиенС(?:РѕРІ|Р°)?|(?:сколько|СЃРєРѕРєР°|СЃРєРѕРє)\s+(?:Сѓ\s+нас\s+)?РїРѕРєСѓРїР°Сел(?:РµР|СЏ)|(?:сколько|СЃРєРѕРєР°|СЃРєРѕРє)\s+(?:Сѓ\s+нас\s+)?СЃРјРµСан(?:РЅСС|СРµ)\s+РєРѕРЅСрагенС(?:РѕРІ|Р°)?|заказСРёРє(?:Рё|РѕРІ)\s*,?\s*РїРѕСЃСавСРёРє(?:Рё|РѕРІ))/iu.test(
text
)
) {
return true;
}
if (/(?:разбеР|раздели|СЃСРѕСЂРјРёСЂСѓР\s+СЃРІРѕРґРє).*(?:РєРѕРЅСрагенС|заказСРёРє|РїРѕСЃСавСРёРє|клиенС|РїРѕРєСѓРїР°Сел)/iu.test(text)) {
return true;
}
return false;
}
function hasLifecycleSegmentationSignal(text: string): boolean {
return /(?:вперв|РЅРѕРІ(?:СРµ|СС|СРµ\s+РєРѕРЅСрагенС|СРµ\s+клиенС|СРµ\s+заказСРёРє)|РёСЃСез|СѓСед|СѓСР»|пропал|РѕСвал|Солько\s+РѕРґРёРЅ\s+раз|СЂРѕРІРЅРѕ\s+РѕРґРёРЅ\s+раз|однораз|дольСРµ\s+РІСЃРµС|дольСРµ\s+всего|долгоРРёРІ|самСРµ\s+СЃСарСРµ|СЃСарСРµ\s+РїРѕ\s+СЃРѕССЂСѓРґРЅРёСесСРІСѓ|регуляр|СЌРїРёР·РѕРґРёС|разов(?:СРµ|РѕР|СРµ\s+РїРѕСЃСавСРёРє)|давно\s+РЅРµ\s+использ|неиспольз|РїРѕСРѕРј\s+пересСал)/iu.test(
text
);
}
function hasCounterpartyDebtLongevitySignal(text: string): boolean {
const hasCounterpartyLexeme =
/(?:заказСРёРє(?:РѕРІ|Р°|Рё)?|клиенС(?:РѕРІ|Р°|С)?|РїРѕРєСѓРїР°Сел(?:РµР|СЏ|Рё)?|РєРѕРЅСрагенС(?:РѕРІ|Р°|С)?|customer(?:s)?|client(?:s)?|counterpart(?:y|ies)|buyer(?:s)?)/iu.test(
text
);
const hasDebtLexeme = /(?:долг(?:Рё|РѕРІ|Р°|Сѓ)?|задолР(?:енносССЊ|енносСРё|енносССЏРј|ал|али)?|РїСЂРѕСЃСЂРѕС|СРІРѕСЃС)/iu.test(text);
const hasLongevityCue =
/(?:долгоРРёРІ|долгоРРёСел|несколько\s+месяС|РїРѕ\s+годам|дольСРµ|леС|РіРѕРґ(?:ам|Р°|Сѓ|С)?|РЅР°\s+СЌСРѕС\s+моменС|длиСельн)/iu.test(
text
);
return hasCounterpartyLexeme && hasDebtLexeme && hasLongevityCue;
}
function hasCounterpartyActivityLifecycleSignal(text: string): boolean {
if (hasCustomerRevenueAndPaymentsSignal(text) || hasSupplierPayoutsProfileSignal(text)) {
return false;
}
const hasActivityAgeCue =
/(?:сколько\s+леС\s+акСРёРІРЅРѕСЃСРё|сколько\s+леС\s+РІ\s+базе|возрасС\s+акСРёРІРЅРѕСЃСРё|перв(?:ая|СР|РѕРµ)\s+(?:акСРёРІРЅРѕСЃССЊ|плаСРµР|РїРѕСЃСупление|докуменС)|последн(?:СЏСЏ|РёР|ее)\s+акСРёРІРЅРѕСЃССЊ|СЃ\s+какого\s+РіРѕРґР°\s+акСРёРІ)/iu.test(
text
);
const hasActivityAgeAnchor =
/(?:компан|РєРѕРЅСрагенС|организаС|РѕРѕРѕ|ао|зао|РёРї|РїРѕ\s+[a-zР°-СЏ0-9"«»().,_-]{3,}|РІ\s+базе\s+1СЃ|РІ\s+1СЃ\s+базе)/iu.test(
text
);
if (hasActivityAgeCue && hasActivityAgeAnchor) {
return true;
}
const hasPaymentRiskLexeme =
/(?:РЅРµ\s+плаС(?:РёС|СЏС|РёР»|или)|без\s+оплаС|оплаС(?:С|Р°)?\s+РЅРµС|РЅРµС\s+оплаС|задерР(?:РёРІР°|Рє)|РїСЂРѕСЃСЂРѕС|задолР|\bдолг(?:Рё|РѕРІ|Р°|Сѓ)?\b)/iu.test(
text
);
if (hasPaymentRiskLexeme) {
return false;
}
if ((hasDocumentSignal(text) || hasBankOperationSignal(text)) && !hasLifecycleSegmentationSignal(text)) {
return false;
}
if (hasAny(text, COUNTERPARTY_ACTIVITY_LIFECYCLE_HINTS)) {
return true;
}
if (/(?:сколько|скока|скок)\s+/iu.test(text) && !hasLifecycleSegmentationSignal(text)) {
return false;
}
const hasCounterpartyLexeme = /(?:заказСРёРє(?:РѕРІ|Р°|Рё)?|клиенС(?:РѕРІ|Р°|С)?|РїРѕРєСѓРїР°Сел(?:РµР|СЏ|Рё)?|РєРѕРЅСрагенС(?:РѕРІ|Р°|С)?|РїРѕСЃСавСРёРє(?:РѕРІ|Р°|Рё)?|customer(?:s)?|client(?:s)?|counterpart(?:y|ies)|supplier(?:s)?|vendor(?:s)?)/iu.test(
text
);
const hasActivityLexeme =
/(?:рабоСал(?:Рё)?|рабоСР°(?:РµС|СЋС)|акСРёРІРЅ(?:СРµ|СС|Р°|Рѕ)?|СЃРѕССЂСѓРґРЅРёСал(?:Рё)?|Р±Сли\s+РІ\s+рабоСРµ|active|использ(?:овал(?:Рё|РѕСЃСЊ)?|СѓСЋССЃСЏ|ован(?:С|Рѕ)?))/iu.test(
text
);
const hasTimeWindowLexeme =
/(?:Р·Р°\s+РІСЃ[РµС]\s+время|all\s+time|\b(?:19|20)\d{2}\b|(?:^|[^\d])\d{2}\s*(?:Рі(?:РѕРґ|РѕРґР°)?|Рі)(?:[^\p{L}\p{N}]|$)|РІ\s+РєРѕРЅРєСЂРµСРЅ(?:РѕРј|СР)\s+РіРѕРґ|Р·Р°\s+РіРѕРґ|РІ\s+РіРѕРґСѓ)/iu.test(
text
);
const hasListVerb = /(?:какие|РєСРѕ|РїРѕРєР°РРё|РІСведи|СЃРїРёСЃРѕРє|list|show)/iu.test(text);
const hasRosterQualifier = /(?:Сѓ\s+нас|РІРѕРѕР±СРµ|РІ\s+баз[РµС]|какие\s+есССЊ|РєСРѕ\s+есССЊ|who\s+are)/iu.test(text);
const hasImplicitCounterpartyQuestion =
/(?:РєСРѕ\s+СЃ\s+нами|РєСРѕ\s+Сѓ\s+нас|РІСЃРµС?\s+зак(?:РѕРІ|Р°|Рё)?|РІСЃРµ\s+заки|РєСРѕ\s+РЅРѕРІ(?:СРµ|СС|СР)\b|РєСРѕ\s+Р±СР»\s+акСивен|самСРµ\s+СЃСарСРµ\s+РїРѕ\s+СЃРѕССЂСѓРґРЅРёСесСРІСѓ)/iu.test(
text
);
const hasListWithWindow = hasCounterpartyLexeme && hasListVerb && hasTimeWindowLexeme;
if (hasListWithWindow) {
return true;
}
if (hasCounterpartyLexeme && hasListVerb && hasRosterQualifier) {
return true;
}
if (hasCounterpartyLexeme && hasLifecycleSegmentationSignal(text)) {
return true;
}
if (hasImplicitCounterpartyQuestion && (hasLifecycleSegmentationSignal(text) || hasTimeWindowLexeme || hasActivityLexeme)) {
return true;
}
if (!hasCounterpartyLexeme && hasListVerb && hasLifecycleSegmentationSignal(text) && /\bРєСРѕ\b/iu.test(text)) {
return true;
}
return hasCounterpartyLexeme && hasActivityLexeme && (hasTimeWindowLexeme || hasListVerb);
}
function hasCounterpartyShipmentItemFlowSignal(text: string): boolean {
const hasSelectedObjectInventoryCue = /(?:РїРѕ\s+СЌСРѕР\s+РїРѕР·РёСРё(?:Рё|СЏ|СЋ)|РїРѕ\s+СЌСРѕРјСѓ\s+Совару|РїРѕ\s+РЅРµР|РїРѕ\s+нему|РїРѕ\s+РЅРёРј|selected\s+object|РїРѕ\s+РІСбранному\s+объекССѓ)/iu.test(
text
);
if (hasSelectedObjectInventoryCue) {
return false;
}
const hasNamedTailAfterShipmentCue =
/(?:РѕСРіСЂСѓР(?:ал|али|ено)|РїРѕСЃСав(?:лял|ляли|РёР»|или)|РїСЂРёРІРѕР·(?:РёР»|или)|продал)\s+[a-zР°-СЏС][a-zР°-СЏС0-9._-]{2,}/iu.test(
text
);
const hasPartySignal =
hasPartyAnchorMention(text) ||
hasLooseByAnchorMention(text) ||
hasImplicitCounterpartyAnchorAroundDocs(text) ||
hasHeuristicCounterpartyAnchor(text);
if (!hasPartySignal && !hasNamedTailAfterShipmentCue) {
return false;
}
const hasInboundShipmentCue =
/(?:ССРѕ\s+нам\s+(?:РѕСРіСЂСѓР(?:ал|али|ено)|РїРѕСЃСав(?:лял|ляли|РёР»|или)|РїСЂРёРІРѕР·(?:РёР»|или)|продал)|РєСРѕ\s+нам\s+РїРѕСЃСав(?:лял|РёР»)|ССРѕ\s+РїРѕСЃСав(?:лял|или)\s+нам|ССРѕ\s+нам\s+РїРѕСЃСавили)/iu.test(
text
);
const hasItemOrServiceCue =
/(?:како(?:Р|Рµ|РіРѕ|РјСѓ)\s+Совар|каки(?:Рµ|С)\s+Совар|какую\s+услуг|какие\s+услуг|Совар\s+или\s+услуг|РїРѕР·РёСРё(?:СЋ|Рё|СЏС)?)/iu.test(
text
);
return hasInboundShipmentCue || hasItemOrServiceCue;
}
function hasContractUsageOverviewSignal(text: string): boolean {
if (hasAny(text, CONTRACT_USAGE_OVERVIEW_HINTS)) {
return true;
}
if (
/(?:сколько\s+(?:всего\s+)?(?:РґРѕРіРѕРІРѕСЂ|РєРѕРЅСракС)(?:РѕРІ|Р°)?(?:\s+заведен[РѕС])?|(?:РґРѕРіРѕРІРѕСЂРЅ(?:ая|РѕР)|РєРѕРЅСракСРЅ(?:ая|РѕР))\s+баз[Р°С]).*(?:сколько|used|использ)/iu.test(
text
)
) {
return true;
}
if (
/(?:сколько\s+РёР·\s+(?:РґРѕРіРѕРІРѕСЂ|РєРѕРЅСракС)(?:РѕРІ|Р°)?\s+(?:реально\s+)?использ(?:ован[РѕС]|овал(?:Рё|РѕСЃСЊ)?))/iu.test(text)
) {
return true;
}
if (/(?:total\s+vs\s+used|used\s+vs\s+total).*(?:РґРѕРіРѕРІРѕСЂ|РєРѕРЅСракС|contract)?/iu.test(text)) {
return true;
}
if (/(?:какие\s+(?:РґРѕРіРѕРІРѕСЂ|РєРѕРЅСракС)(?:С|Р°)?).*(?:давно\s+РЅРµ\s+использ|неиспольз|РїСЂРѕССѓС|мерСРІ|РјССЂСРІ|stale|unused)/iu.test(text)) {
return true;
}
return false;
}
function hasCustomerRevenueAndPaymentsSignal(text: string): boolean {
if (hasAny(text, CUSTOMER_REVENUE_AND_PAYMENTS_HINTS)) {
return true;
}
if (hasContractAnchorSignal(text)) {
return false;
}
const hasFuzzyCustomerLexeme = hasFuzzyLexeme(text, ["клиент", "заказчик", "покупател", "customer", "client"]);
const hasFuzzySupplierLexeme = hasFuzzyLexeme(text, ["поставщик", "supplier", "vendor"]);
const hasCounterpartyLexeme = /(?:РєРѕРЅСрагенС(?:РѕРІ|Р°|С)?|counterpart(?:y|ies)|компан(?:Рё|РёСЏ|РёРё|РёСЋ)|организаС(?:Рё|РёСЏ|РёРё|РёСЋ)|partner(?:s)?)/iu.test(
text
);
const hasSpecificCounterpartyAnchor =
hasLooseByAnchorMention(text) ||
hasHeuristicCounterpartyAnchor(text) ||
/(?:РїРѕ\s+(?:клиенС(?:Сѓ|Р°)?|заказСРёРє(?:Сѓ|Р°)?|РїРѕРєСѓРїР°Сел(?:СЋ|СЏ)|customer|client)\s+[a-zР°-СЏС0-9])/iu.test(text);
const asksWhoPays = /(?:РєСРѕ\s+(?:нам\s+)?(?:(?:больСРµ|СР°СРµ)\s+)?плаС(?:РёС|СЏС)?)/iu.test(text);
const asksCustomerGroup =
/(?:клиенС(?:РѕРІ|Р°|С)?|заказСРёРє(?:РѕРІ|Р°|Рё)?|РїРѕРєСѓРїР°Сел(?:РµР|СЏ|Рё)?|customer(?:s)?|client(?:s)?)/iu.test(text) ||
hasFuzzyCustomerLexeme ||
asksWhoPays;
const asksCounterpartySource = /(?:СЃ\s+какиС|РѕС\s+какиС|РѕС\s+РєРѕРіРѕ|from\s+which|from\s+who)/iu.test(text);
const asksIncomingFlow = /(?:РїСЂРёСРѕРґ|РїРѕСЃСуплен|РІСРѕРґСЏС|Р·Р°Сислен|inflow|incoming)/iu.test(text);
const asksWhoBringsMostMoney =
/(?:РєСРѕ\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(
text
);
const asksRevenueTotal = /(?:сколько|СЃРєРѕРєР°|СЃРєРѕРє).*(?:денег|РІССЂСѓСРє|РґРѕСРѕРґ|зарабоС|РѕР±РѕСЂРѕС)/iu.test(text);
const asksOverallTurnover = /(?:РѕР±С(?:РёР|РёРµ|ая)\s+РѕР±РѕСЂРѕС|РѕР±С(?:ая|РёР)\s+РІССЂСѓСРє|total\s+turnover|turnover\s+total)/iu.test(text);
const asksMajorShare =
/(?:РѕСЃРЅРѕРІРЅ(?:СѓСЋ|ая|СРµ|РѕР)\s+СасС|больС(?:СѓСЋ|ая|РёРµ)\s+СасС|львин(?:ая|СѓСЋ)\s+РґРѕР»[СЏСЋ]|клюСев(?:СѓСЋ|ая)\s+СасС)/iu.test(
text
);
const asksValue =
/(?:РґРѕСРѕРґРЅ|РІССЂСѓСРє|РїСЂРёСРѕРґ|РїРѕСЃСуплен|РІСРѕРґСЏС|Р·Р°Сислен|оплаС|плаС(?:РµР|СР|РµРРЅ|РµРРµР|РµРР°|РёС|СЏС)|деньг|денег|зарабоС|РѕР±РѕСЂРѕС|Сек|сделк|Р±СЋРґРРµС|занес|занССЃ|принес|РїСЂРёРЅССЃ|ликвидн|revenue|inflow|deal|turnover|liquid)/iu.test(
text
);
const asksRankOrTop = /(?:СРѕРї|top|сам(?:СР|ая|РѕРµ|СРµ)|РєСЂСѓРїРЅ|мален|РРёСЂРЅ|мелк|больСРµ\s+всего|СР°СРµ\s+всего|наибольС|максимальн)/iu.test(
text
);
const asksCountOnly = /(?:сколько|скока|скок)\s+/iu.test(text) && !asksValue;
if (asksCountOnly) {
return false;
}
if (hasSpecificCounterpartyAnchor && !asksRankOrTop) {
return false;
}
if (asksCustomerGroup && (asksValue || asksRankOrTop)) {
return true;
}
if (!hasFuzzySupplierLexeme && hasCounterpartyLexeme && asksRankOrTop && (asksValue || asksWhoPays)) {
return true;
}
if (!hasFuzzySupplierLexeme && asksWhoPays && (asksRankOrTop || hasCounterpartyLexeme)) {
return true;
}
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;
}
if (asksCounterpartySource && asksValue) {
return true;
}
if (!hasFuzzySupplierLexeme && (asksCustomerGroup || hasCounterpartyLexeme) && asksMajorShare && asksValue) {
return true;
}
if (!hasFuzzySupplierLexeme && asksIncomingFlow && asksRankOrTop) {
return true;
}
if (!hasFuzzySupplierLexeme && asksDealBudgetRanking) {
return true;
}
return false;
}
function hasSupplierPayoutsProfileSignal(text: string): boolean {
if (hasAny(text, SUPPLIER_PAYOUTS_PROFILE_HINTS)) {
return true;
}
if (hasContractAnchorSignal(text)) {
return false;
}
const hasFuzzySupplierLexeme = hasFuzzyLexeme(text, ["поставщик", "supplier", "vendor"]);
const hasSpecificCounterpartyAnchor =
hasLooseByAnchorMention(text) ||
hasHeuristicCounterpartyAnchor(text) ||
/(?:РїРѕ\s+(?:РїРѕСЃСавСРёРє(?:Сѓ|Р°)?|supplier|vendor)\s+[a-zР°-СЏС0-9])/iu.test(text);
const asksSupplierGroup =
/(?:РїРѕСЃСавСРёРє(?:РѕРІ|Р°|Рё)?|supplier(?:s)?|vendor(?:s)?|Рє[ао]РјСѓ\s+РјС)/iu.test(text) ||
hasFuzzySupplierLexeme ||
/(?:РєРѕРјСѓ\s+СѓСло|РєРѕРјСѓ\s+плаСили|РєРѕРјСѓ\s+заплаСили)/iu.test(text);
const asksPayoutValue =
/(?:РІСплаС|РёСЃСРѕРґСЏС|списан|заплаС|СѓСло|СЃРіСЂСѓР·РёР»|сгрузили|перевел|перевСР»|РѕСдали|плаСРµР|плаССР|outflow|payout)/iu.test(
text
);
const asksRankOrTop = /(?:СРѕРї|top|сам(?:СР|ая|РѕРµ|СРµ)|РєСЂСѓРїРЅ|больСРµ\s+всего|СР°СРµ\s+всего|максимальн|наибольС)/iu.test(
text
);
const asksCountOnly = /(?:сколько|скока|скок)\s+/iu.test(text) && !asksPayoutValue;
if (asksCountOnly) {
return false;
}
if (hasSpecificCounterpartyAnchor && !asksRankOrTop) {
return false;
}
return asksSupplierGroup && (asksPayoutValue || asksRankOrTop);
}
function hasContractUsageAndValueSignal(text: string): boolean {
if (hasAny(text, CONTRACT_USAGE_AND_VALUE_HINTS)) {
return true;
}
if (!/(?:РґРѕРіРѕРІРѕСЂ(?:РѕРІ|Р°|С)?|РєРѕРЅСракС(?:РѕРІ|Р°|С|Сѓ|РѕРј|Рµ)?|contract(?:s)?)/iu.test(text)) {
return false;
}
if (hasContractUsageOverviewSignal(text)) {
return false;
}
const asksStructure = /(?:нескольк(?:РёРјРё|РёС|РёРµ|Рѕ)?\s+(?:РґРѕРіРѕРІРѕСЂ|РєРѕРЅСракС)|мульСРёРґРѕРіРѕРІРѕСЂ|РєРѕРЅСрагенС(?:РѕРІ|С)?.*нескольк(?:РёРјРё|РёС|РёРµ|Рѕ)\s+(?:РґРѕРіРѕРІРѕСЂ|РєРѕРЅСракС)|какие\s+(?:РґРѕРіРѕРІРѕСЂ|РєРѕРЅСракС)(?:С|Р°)?\s+акСРёРІРЅ|рабоС(?:РёРµ|РёС)\s+(?:РґРѕРіРѕРІРѕСЂ|РєРѕРЅСракС))/iu.test(
text
);
const asksValue =
/(?:РѕР±РѕСЂРѕС|Р±СЋРґРРµС|СЃСѓРјРј|СЃСРѕРёРј|value|turnover|amount|revenue|РєСЂСѓРїРЅ|мелк|РјРёРЅРёРј|максим)/iu.test(text);
const asksRank = /(?:СРѕРї|top|ранк|rank|сам(?:СР|ая|РѕРµ|СРµ))/iu.test(text);
return asksStructure || asksValue || asksRank;
}
function hasContractListByCounterpartySignal(text: string): boolean {
const hasContractLexeme = /(?:РґРѕРіРѕРІРѕСЂ(?:Р°|Сѓ|РѕРј|Рµ|С)?|РєРѕРЅСракС(?:Р°|Сѓ|РѕРј|Рµ|С)?|contracts?|contract)/iu.test(text);
if (!hasContractLexeme) {
return false;
}
// If user explicitly asks for documents, keep routing in document-by-contract/counterparty lane.
if (hasDocumentSignal(text)) {
return false;
}
if (hasContractUsageOverviewSignal(text) || hasOpenContractsListSignal(text)) {
return false;
}
if (hasContractNumberLikeToken(text)) {
return false;
}
if (hasBankOperationSignal(text)) {
return false;
}
const hasListVerb = /(?:РїРѕРєР°РРё|РІСведи|СЃРїРёСЃРѕРє|какие|show|list)/iu.test(text);
const hasAllQualifier = /(?:\ball\b|\bРІСЃРµ\b|РІСЃС)/iu.test(text);
const hasCounterpartyAnchor =
hasPartyAnchorMention(text) ||
hasLooseByAnchorMention(text) ||
hasHeuristicCounterpartyAnchor(text);
if (!hasCounterpartyAnchor) {
return false;
}
return hasListVerb || hasAllQualifier || hasAny(text, CONTRACT_LIST_BY_COUNTERPARTY_HINTS);
}
function hasDocumentsByAccountDrilldownSignal(text: string): boolean {
const hasAccountLexeme = hasAccountNumberAnchor(text) || hasCompactAccountCodeToken(text);
const hasDocLexeme = /(?:докуменС|РґРѕРє(?:Рё|ам|Р°С|РѕРІ|Р°)?|docs?|documents?|doki|docy|doci)/iu.test(text);
const hasDrilldownVerb = /(?:раскроР|раскрС|разлоР|разверн|докуменСами|РїРѕ\s+докуменС)/iu.test(text);
const hasSameDate = /(?:РЅР°\s+ССѓ\s+РРµ\s+РґР°С[ауеС]|same\s+date|the\s+same\s+date)/iu.test(text);
return hasAccountLexeme && hasDocLexeme && (hasDrilldownVerb || hasSameDate);
}
function hasOpenContractsListSignal(text: string): boolean {
const hasContractLexeme = text.includes("договор") || text.includes("контракт") || text.includes("contract") || text.includes("dogovor");
const hasOpenLexeme = /(?:незакрСС|РЅРµ\s+закрСС|РѕСРєСЂСС|open|unclosed)/iu.test(text);
if (!hasContractLexeme || !hasOpenLexeme) {
return false;
}
// Query about a specific contract should stay in open-items lane.
if (hasContractNumberLikeToken(text)) {
return false;
}
// Debt/tail wording indicates open-items intent, not contract list.
if (/(?:долг|задолР|СРІРѕСЃС|РїРѕР·РёС|open\s+items|unclosed\s+items|взаиморасСРµС|взаиморасССС)/iu.test(text)) {
return false;
}
return true;
}
function hasSupplierTailRiskSignal(text: string): boolean {
const hasSupplier = /(?:РїРѕСЃСавСРёРє|supplier|vendor)/iu.test(text);
const hasTail = /(?:СРІРѕСЃС|РІРёСЃСЏС|незакрСС|РЅРµ\s+закрСРІ|задолР|долг|РїСЂРѕСЃСЂРѕС|СЃС[РµС]С)/iu.test(text);
const hasRisk = /(?:СЃРёСЃСемаСРёС|регулярн|проблем|Сревог|РЅРµ\s+разов|больСРµ\s+РїРѕСРѕР)/iu.test(text);
const hasPeriodCue = /(?:РЅР°\s+РєРѕРЅРµС\s+(?:месяС|период)|РєРѕРЅРµС\s+месяС|пару\s+месяС|несколько\s+месяС|больСРµ\s+месяС)/iu.test(text);
return hasSupplier && hasTail && (hasRisk || hasPeriodCue);
}
function hasPayablesDebtLifecycleSignal(text: string): boolean {
const hasOweSignal =
/(?:РєРѕРјСѓ\s+РјС\s+РґРѕР»Р(?:ен|РЅС|СЌРЅС|СЌРЅР°|СЌРЅРѕ)?|РјС\s+РґРѕР»Р(?:ен|РЅС|СЌРЅС|СЌРЅР°|СЌРЅРѕ)?|РєРѕРјСѓ\s+РґРѕР»Р(?:ен|РЅС|СЌРЅС|СЌРЅР°|СЌРЅРѕ)?|РґРѕР»Р[РЅСЌ](?:С|Р°|Рѕ)?\s+(?:заплаС|оплаС|переСРёСЃ)|Рє\s+оплаСРµ|РЅР°\s+оплаССѓ|who\s+we\s+owe|owe\s+to|payables?|кредиСРѕСЂ(?:[Р°-СЏС]{0,6})?)/iu.test(
text
);
if (!hasOweSignal) {
return false;
}
const hasPastPaymentSignal = /(?:заплаСРёР»(?:Рё)?|плаСРёР»(?:Рё)?|РєРѕРјСѓ\s+СѓСло|РІСплаСРёР»(?:Рё)?|списан|outflow|payout)/iu.test(text);
const hasTopRankingSignal = /(?:СРѕРї|top|больСРµ\s+всего|сам(?:СР|ая|РѕРµ|СРµ)|наибольС|максимальн)/iu.test(text);
if (hasPastPaymentSignal && hasTopRankingSignal) {
return false;
}
return true;
}
function hasReceivablesDebtLifecycleSignal(text: string): boolean {
const hasOweUsSignal =
/(?:РєСРѕ\s+нам\s+РґРѕР»Р(?:ен|РЅС|СЌРЅС|СЌРЅР°|СЌРЅРѕ)?|РєСРѕ\s+РґРѕР»Р(?:ен|РЅС|СЌРЅС|СЌРЅР°|СЌРЅРѕ)?\s+нам|нам\s+РґРѕР»Р(?:ен|РЅС|СЌРЅС|СЌРЅР°|СЌРЅРѕ)?|РґРѕР»РРЅРёРє(?:[Р°-СЏС]{0,6})?|дебиСРѕСЂ(?:[Р°-СЏС]{0,6})?|дебиСРѕСЂСЃРє(?:[Р°-СЏС]{0,6})?|задолР|долг(?:Рё|РѕРІ|Р°|Сѓ)?|Рє\s+полуСению|РЅР°\s+РїРѕСЃСупление|Рє\s+РІР·Ссканию|who\s+owes\s+us|receivables?)/iu.test(
text
);
if (!hasOweUsSignal) {
return false;
}
const hasPastInflowSignal = /(?:РїСЂРёРЅ[РµС]СЃ|зан[РµС]СЃ|РїРѕСЃССѓРїРёР»|РїСЂРёСРѕРґ|inflow|paid\s+us|already\s+paid)/iu.test(text);
const hasTopRankingSignal = /(?:СРѕРї|top|больСРµ\s+всего|сам(?:СР|ая|РѕРµ|СРµ)|наибольС|максимальн)/iu.test(text);
if (hasPastInflowSignal && hasTopRankingSignal) {
return false;
}
return true;
}
function hasReceivablesLatencyRiskSignal(text: string): boolean {
const hasBuyer = /(?:РїРѕРєСѓРїР°Сел|клиенС|заказСРёРє|customer|buyer)/iu.test(text);
const hasCounterparty = /(?:РєРѕРЅСрагенС|counterparty|partner)/iu.test(text);
const hasPayment = /(?:оплаС|плаСРµР|плаССР|payment)/iu.test(text);
const hasShipment = /(?:РѕСправк|РѕСРіСЂСѓР·|реализ|shipment|delivery)/iu.test(text);
const hasDelay = /(?:длинн|долг|РїСЂРѕСЃСЂРѕС|задерР|РІРёСЃСЏС|Сревог|too\s+long|late)/iu.test(text);
const hasOverdueDeadlineCue =
/(?:СЃСЂРѕРє(?:Рё|Р°)?(?:\s+оплаС[СС]?)?[\s\S]{0,24}(?:РїСЂРѕС|РІСС|РёСЃСек|РёСЃССРє)|СЃСЂРѕРє(?:Рё|Р°)?\s+давно\s+РїСЂРѕСР»|давно\s+РїРѕСЂР°\s+оплаС|давно\s+РЅРµ\s+оплаС)/iu.test(
text
);
const hasNonPayment = /(?:РЅРµ\s+плаС(?:РёС|СЏС|РёР»|или)|РЅРµ\s+оплаС|РЅРµ\s+оплаС|без\s+оплаС|оплаС(?:С|Р°)?\s+РЅРµС|РЅРµС\s+оплаС|неоплаС)/iu.test(text);
const hasPaymentShipmentImbalance =
/(?:оплаС(?:ено|ен[аоС]?|РёРІР°ССЊ|РёРІР°ССЊ)?\s+меньСРµ[\s\S]{0,36}РѕСРіСЂСѓР|недоплаС[\s\S]{0,36}РѕСРіСЂСѓР|РѕСРіСЂСѓР[\s\S]{0,36}оплаС(?:ено|ено\s+меньСРµ))/iu.test(
text
);
const hasNegativeSaldoRisk = /(?:сальд[РѕР°]\s+(?:СѓРРµ\s+)?РѕССЂРёСР°С|РјРёРЅСѓСЃРѕРІ(?:РѕРµ|РѕР)\s+сальдо|сальдо\s+РІ\s+РјРёРЅСѓСЃ)/iu.test(text);
const hasPeriodOrRiskCue =
/(?:Р·Р°\s+СекуС|РЅР°\s+РєРѕРЅРµС|Сревог|РїСЂРѕСЃСЂРѕС|задерР|долг|длинн|несколько\s+месяС|больСРµ\s+месяС)/iu.test(text) ||
hasOverdueDeadlineCue ||
hasNegativeSaldoRisk;
const hasBetweenShipmentAndPayment =
/РјРµРРґСѓ[\s\S]{0,80}(?:РѕСправк|РѕСРіСЂСѓР·|реализ)[\s\S]{0,80}(?:оплаС|плаСРµР|плаССР|payment)/iu.test(text);
if (
hasBuyer &&
hasPayment &&
((hasShipment && (hasDelay || hasOverdueDeadlineCue)) || hasBetweenShipmentAndPayment || hasPaymentShipmentImbalance)
) {
return true;
}
if ((hasBuyer || hasCounterparty) && hasPaymentShipmentImbalance) {
return true;
}
return (hasBuyer || hasCounterparty) && hasNonPayment && hasPeriodOrRiskCue;
}
function hasSettlementGapSignal(text: string): boolean {
const hasPayment = /(?:плаСРµР|плаССР|оплаС|списани|РїРѕСЃСуплен|payment)/iu.test(text);
const hasDocument = /(?:РґРѕРє(?:Рё|уменС|уменСС|уменСРѕРІ)|docs?|documents?)/iu.test(text);
const hasShipment = /(?:РѕСРіСЂСѓР·|реализ|shipment|delivery|Совар|услуг)/iu.test(text);
const hasAdvance = /(?:аванс|предоплаС)/iu.test(text);
const hasClosureLexeme = /(?:закрССРё|взаиморасС|акС|СЃС[РµС]С(?:РѕРІ|Р°|С)?)/iu.test(text);
const hasNoDocumentForClosing =
/(?:РЅРµС|без)\s+(?:РґРѕРє(?:Рё|уменС|уменСС|уменСРѕРІ)|закрСваюС)/iu.test(text) &&
hasClosureLexeme;
const hasNoDocumentForClosingReversed =
/(?:РґРѕРє(?:Рё|уменС|уменСС|уменСРѕРІ)|закрСваюС)[\s\S]{0,48}(?:РЅРµС|без)/iu.test(text) &&
hasClosureLexeme;
const hasNoPayments =
/(?:РЅРµС|без)\s+(?:оплаС|плаСРµР|плаССР|payment)/iu.test(text) ||
/(?:оплаС|плаСРµР|плаССР|payment)\s+РЅРµС/iu.test(text);
const hasDocsWithoutPayments = hasDocument && hasNoPayments;
const hasPaymentsWithoutClosingDocs = hasPayment && (hasNoDocumentForClosing || hasNoDocumentForClosingReversed);
const hasPaymentsWithoutSettlementClosure =
hasPayment &&
/(?:без|РЅРµС)\s+закрССРё(?:СЏ|Р)?(?:\s+взаиморасС[РµС]СРѕРІ)?/iu.test(text) &&
hasClosureLexeme;
const hasShipmentWithoutClosingDocs =
hasShipment &&
(hasNoDocumentForClosing ||
hasNoDocumentForClosingReversed ||
/(?:без|РЅРµС)\s+РґРѕРє(?:Рё|уменС(?:РѕРІ|С|Р°)?)\s+(?:для\s+)?(?:РёС\s+)?закрССРё/u.test(text));
const hasClosingWithoutSupportingDocs =
hasClosureLexeme &&
/(?:без|РЅРµС)\s+РїРѕРґСверРдаюС(?:РёС|его|РёРµ)?\s+РґРѕРє(?:Рё|уменС(?:РѕРІ|С|Р°)?)/iu.test(text);
const hasAdvanceStuckRisk =
/(?:зависС(?:РёР|РёРµ|ая|РёРµ\s+аванс)|давно\s+РїРѕСЂР°\s+закрСССЊ|РїРѕСЂР°\s+закрСРІР°ССЊ|перепривяз(?:Р°ССЊ|Рє)|СЃРїРёСЃСРІ(?:Р°ССЊ|ани|ан)|нереальн)/iu.test(
text
);
const hasUnclosedAdvanceGap =
hasAdvance &&
(/(?:РЅРµ\s+закрСС|незакрСС|долго\s+РЅРµ\s+закрСС|давно\s+РЅРµ\s+закрСС|давно\s+РїРѕСЂР°\s+закрСССЊ)/iu.test(text) ||
hasAdvanceStuckRisk ||
hasNoDocumentForClosing ||
hasNoDocumentForClosingReversed);
return (
hasPaymentsWithoutClosingDocs ||
hasPaymentsWithoutSettlementClosure ||
hasDocsWithoutPayments ||
hasShipmentWithoutClosingDocs ||
hasClosingWithoutSupportingDocs ||
hasUnclosedAdvanceGap
);
}
function hasReconciliationMismatchSignal(text: string): boolean {
const hasCounterparty =
/(?:РєРѕРЅСрагенС|РїРѕСЃСавСРёРє|клиенС|РїРѕРєСѓРїР°Сел|customer|supplier|counterparty)/iu.test(text);
const hasReconciliationLexeme = /(?:акС(?:Р°|РѕРј|Р°С)?\s+свер(?:Рє|РѕРє)|свер(?:Рє|РѕРє))/iu.test(text);
const hasMismatchLexeme =
/(?:РЅРµ\s+совпад|несовпад|расСРѕРРґ|расСРѕРґ|РЅРµ\s+СЃСРѕРґ|несСРѕРґ|разъеС|разниС|РЅРµ\s+Р±СЊ[РµС]С)/iu.test(text);
const hasBalanceLexeme = /(?:сальд|РѕСЃСР°С|баланс|saldo|balance)/iu.test(text);
const hasLookupVerb = /(?:РїРѕРєР°РРё|РІСведи|РЅР°РРґ[РёСЊ]|show|list)/iu.test(text);
const hasInterrogativeLookup = /(?:РїРѕ\s+каким|Сѓ\s+РєРѕРіРѕ|какие|какоР|РєСРѕ|РіРґРµ)/iu.test(text);
return (
hasCounterparty &&
hasReconciliationLexeme &&
hasMismatchLexeme &&
hasBalanceLexeme &&
(hasLookupVerb || hasInterrogativeLookup)
);
}
function isLikelyCounterpartyToken(rawToken: string): boolean {
const token = String(rawToken ?? "").trim().toLowerCase();
if (!token || token.length < 2) {
return false;
}
if (/^\d+$/.test(token)) {
return false;
}
if (/^(?:19|20)\d{2}$/.test(token)) {
return false;
}
const stopWords = new Set([
"Р·Р°",
"СЃ",
"РїРѕ",
"РЅР°",
"Рё",
"или",
"РґРѕРє",
"РґРѕРєРё",
"РґРѕРєРё?",
"документ",
"документы",
"документов",
"документами",
"документу",
"документе",
"документа",
"документах",
"докам",
"доками",
"количество",
"количеству",
"количества",
"количеством",
"активный",
"активного",
"активности",
"пассивный",
"пассивного",
"пассивности",
"наименее",
"минимальный",
"РјРёРЅРёРјСѓРј",
"реже",
"редкий",
"банк",
"банковские",
"операции",
"платежи",
"платеж",
"платёж",
"контрагент",
"контрагенту",
"контрагента",
"компания",
"компании",
"организация",
"организации",
"РіРѕРґ",
"РіРѕРґР°",
"Рі",
"плс",
"pls",
"РїР¶",
"пжлст",
"пожалуйста",
"есть",
"Р¶Рµ",
"сводные",
"сводный",
"сводная",
"СЃРІРѕРґРЅСѓСЋ",
"СЃРІРѕРґРЅРѕРј",
"СЃРІРѕРґРЅРѕРіРѕ",
"СЃРІРѕРґРЅРѕРјСѓ",
"неуказанному",
"неуказанный",
"неуказанная",
"неуказанное",
"указанному",
"указанный",
"указанная",
"указанное",
"объекту",
"объект",
"бля",
"блять",
"епт",
"ёпт",
"епта",
"нах",
"нахуй",
"связанным",
"связанные",
"связанных",
"связанному",
"related",
"linked",
"этомуже",
"томуже"
]);
return !stopWords.has(token);
}
function hasPartyAnchorMention(text: string): boolean {
return (
text.includes("контраг") ||
text.includes("контра") ||
text.includes("counterparty") ||
text.includes("компан") ||
text.includes("company") ||
text.includes("организац") ||
text.includes("supplier") ||
text.includes("vendor") ||
text.includes("customer") ||
text.includes("client") ||
text.includes("partner") ||
text.includes("поставщик") ||
text.includes("клиент") ||
text.includes("покупател") ||
text.includes("партнер")
);
}
function hasContractAnchorMention(text: string): boolean {
return (
text.includes("РґРѕРіРѕРІРѕСЂ") ||
text.includes("контракт") ||
/\bРґРѕРі\.?\b/iu.test(text) ||
text.includes("РґРѕРі.") ||
text.includes("contract") ||
text.includes("dogovor")
);
}
function hasContractNumberLikeToken(text: string): boolean {
if (/(?:^|[\s([{])(?:в|#|n)\s*[a-zР°-СЏС0-9][a-zР°-СЏС0-9./_-]{1,}(?=$|[\s,.;:!?)\]}])/iu.test(text)) {
return true;
}
const rawTokens = text
.split(/[\s,;:!?()[\]{}"«»]+/u)
.map((token) => token.replace(/^[^\p{L}\p{N}#в]+|[^\p{L}\p{N}./_-]+$/gu, "").trim())
.filter((token) => token.length > 0);
for (const rawToken of rawTokens) {
const token = String(rawToken ?? "").trim();
if (!/^\d{1,6}[./_-]\d{1,6}(?:[./_-]\d{1,6})?$/u.test(token)) {
continue;
}
if (!token) {
continue;
}
if (/^\d{1,2}\.\d{1,2}$/u.test(token)) {
// Likely an account code like 60.01/51.00, not a contract number.
continue;
}
const parts = token.split(/[./_-]+/u).map((part) => Number(part));
if (!parts.every((part) => Number.isFinite(part))) {
return true;
}
if (parts.length === 2) {
const [a, b] = parts;
const yearFirst = a >= 1900 && a <= 2099 && b >= 1 && b <= 12;
const yearSecond = b >= 1900 && b <= 2099 && a >= 1 && a <= 12;
if (yearFirst || yearSecond) {
continue;
}
return true;
}
if (parts.length === 3) {
const [a, b, c] = parts;
const ymd = a >= 1900 && a <= 2099 && b >= 1 && b <= 12 && c >= 1 && c <= 31;
const dmy = c >= 1900 && c <= 2099 && a >= 1 && a <= 31 && b >= 1 && b <= 12;
if (ymd || dmy) {
continue;
}
return true;
}
return true;
}
return false;
}
function hasContractAnchorSignal(text: string): boolean {
if (hasContractAnchorMention(text)) {
return true;
}
// Allow short forms like "19/15" for follow-up prompts if document/bank signal exists.
return hasContractNumberLikeToken(text) && hasDocsOrBankSignal(text);
}
function hasLooseByAnchorMention(text: string): boolean {
const match = text.match(/(?:^|\s)РїРѕ\s+([a-zР°-СЏС][a-zР°-СЏС0-9._-]{1,})(?=[\s,.;:!?)]|$)/iu);
if (!match) {
return false;
}
const token = String(match[1] ?? "").toLowerCase();
if (!token) {
return false;
}
const stopWords = new Set([
"контрагенту",
"контрагента",
"контре",
"компании",
"компанию",
"организации",
"организацию",
"поставщику",
"поставщика",
"клиенту",
"клиента",
"покупателю",
"покупателя",
"партнеру",
"партнера",
"РґРѕРіРѕРІРѕСЂСѓ",
"РґРѕРіРѕРІРѕСЂР°",
"счету",
"счёту",
"дате",
"периоду",
"период",
"документам",
"докам",
"количество",
"количеству",
"количества",
"количеством",
"активности",
"пассивности",
"наименее",
"РјРёРЅРёРјСѓРј"
]);
return !stopWords.has(token);
}
function hasImplicitCounterpartyAnchorAroundDocs(text: string): boolean {
const beforeDocsMatch = text.match(
/(?:^|\s)([a-zР°-СЏС][a-zР°-СЏС0-9._-]{1,})\s+(?:РґРѕРє(?:Рё|СѓРј(?:енС(?:С|РѕРІ|ам|Р°)?)?)|docs?|documents?|doki|docy|doci)(?=[\s,.;:!?)]|$)/iu
);
if (beforeDocsMatch && isLikelyCounterpartyToken(String(beforeDocsMatch[1] ?? ""))) {
return true;
}
const afterDocsMatch = text.match(
/(?:РґРѕРє(?:Рё|СѓРј(?:енС(?:С|РѕРІ|ам|Р°)?)?)|docs?|documents?|doki|docy|doci)\s+(?:РїРѕ\s+)?([a-zР°-СЏС][a-zР°-СЏС0-9._-]{1,})(?=[\s,.;:!?)]|$)/iu
);
if (afterDocsMatch && isLikelyCounterpartyToken(String(afterDocsMatch[1] ?? ""))) {
return true;
}
return false;
}
function hasDocsOrBankSignal(text: string): boolean {
return /(?:РґРѕРє(?:Рё|уменС|уменСС|уменСРѕРІ)|docs?|documents?|doki|docy|doci|банк|РІСРїРёСЃРє|плаСРµР|плаССР|оплаС|transactions?|bank\s+ops|bank\s+operations?)/iu.test(
text
);
}
function hasBankOperationSignal(text: string): boolean {
return hasAny(text, BANK_OPERATION_CORE_HINTS) || hasAny(text, BANK_OPERATIONS_BY_COUNTERPARTY_HINTS) || hasAny(text, BANK_OPERATIONS_BY_CONTRACT_HINTS);
}
function hasDocumentSignal(text: string): boolean {
return (
text.includes("РґРѕРє") ||
text.includes("РґРѕРєРё") ||
text.includes("документ") ||
text.includes("doki") ||
text.includes("docy") ||
text.includes("doci") ||
text.includes("docs") ||
text.includes("documents")
);
}
function hasHeuristicCounterpartyAnchor(text: string): boolean {
if (!hasDocsOrBankSignal(text) && !hasBankOperationSignal(text)) {
return false;
}
const tokens = String(text ?? "")
.split(/[^a-zР°-СЏС0-9._-]+/iu)
.map((item) => item.trim())
.filter((item) => item.length > 0);
for (const token of tokens) {
const lowered = token.toLowerCase();
if (!isLikelyCounterpartyToken(lowered)) {
continue;
}
if (/^\d{2}$/.test(lowered) || /^\d{4}$/.test(lowered)) {
continue;
}
if (/(?:^Р·Р°$|^for$|^from$|^to$|^РїРѕ$|^СЃ$|^РіРѕРґ$|^РіРѕРґР°$|^Рі$|^year$)/iu.test(lowered)) {
continue;
}
return true;
}
return false;
}
function hasGenericAddressLookupSignal(text: string): boolean {
return (
/\bесССЊ\b/iu.test(text) ||
/\bРїРѕРєР°РРё\b/iu.test(text) ||
/\bРІСведи\b/iu.test(text) ||
/\bкакие\b/iu.test(text) ||
/\bССРѕ(?:-|\s)?СРѕ\b/iu.test(text) ||
/Р·Р°\s+любоР\s+период/iu.test(text) ||
/Р·Р°\s+РІСЃ[РµС]\s+время/iu.test(text) ||
/for\s+all\s+time/iu.test(text) ||
/all\s+time/iu.test(text)
);
}
export function hasAccountNumberAnchor(text: string): boolean {
return /(?:account|СЃС‡[её]С|счет)\D{0,12}\d{2}(?:[.,]\d{1,2})?/i.test(text);
}
function hasInventoryAccount41Anchor(text: string): boolean {
return /(?:СЃС[РµС]С(?:Р°|Рµ|Сѓ)?|СЃСРµС(?:Р°|Рµ|Сѓ)?)\D{0,12}41(?:[.,]0?1)?/iu.test(text) || /41(?:[.,]0?1)?\D{0,12}(?:СЃС[РµС]С(?:Р°|Рµ|Сѓ)?|СЃСРµС(?:Р°|Рµ|Сѓ)?)/iu.test(text);
}
function hasInventoryAsOfCue(text: string): boolean {
return /(?:СЃРµРСас|СекуС|РЅР°\s+РґР°ССѓ|РїРѕ\s+СЃРѕСЃСРѕСЏРЅРёСЋ|срез|РЅР°\s+РєРѕРЅРµС|date|as\s+of|current|now|today)/iu.test(
text
);
}
function hasInventoryOnHandSignal(text: string): boolean {
const hasColloquialStockSnapshotCue = /(?:ССРѕ|С[РµСРѕ])\s+(?:Сѓ\s+нас\s+)?РЅР°\s+склад(?:Рµ|Сѓ|РѕРј|Р°С)(?=$|[\s,.;:!?])/iu.test(
text
);
const hasStockStateCue = /(?:(?:ССРѕ|С[РµСРѕ])\s+Сам\s+РЅР°\s+склад(?:Рµ|Сѓ|РѕРј|Р°С)|(?:ССРѕ|С[РµСРѕ]).*РїСЂРѕРёСЃСРѕРґ(?:РёС|ило|СЏСее).*(?:РЅР°\s+)?склад(?:Рµ|Сѓ|РѕРј|Р°С)|РїСЂРѕРёСЃСРѕРґ(?:РёС|ило|СЏСее)\s+РЅР°\s+склад(?:Рµ|Сѓ|РѕРј|Р°С)|СЃРёССѓР°С(?:РёСЏ|РёРё)\s+РЅР°\s+склад(?:Рµ|Сѓ|РѕРј|Р°С)|РѕР±СЃСановк(?:Р°|Рё)\s+РЅР°\s+склад(?:Рµ|Сѓ|РѕРј|Р°С)|what(?:'s| is)?\s+(?:there\s+)?(?:on|in)\s+(?:the\s+)?(?:warehouse|stock)|what(?:'s| is)?\s+happening\s+(?:on|in)\s+(?:the\s+)?(?:warehouse|stock))/iu.test(
text
);
const hasAccount41Anchor = hasInventoryAccount41Anchor(text);
const hasStockLexeme =
/(?:склад(?:Рµ|Сѓ|РѕРј|С|РѕРІ)?|warehouse|stock(?:room)?|inventory|on[\s-]?hand)/iu.test(text);
if (!hasStockLexeme && !hasAccount41Anchor) {
return false;
}
if (
hasInventoryProvenanceSignalV2(text) ||
hasInventoryPurchaseDocumentsSignalV2(text) ||
hasInventorySaleTraceSignalV2(text) ||
hasInventoryAgingSignal(text) ||
hasInventoryPurchaseToSaleChainSignal(text)
) {
return false;
}
const hasGoodsLexeme =
/(?:Совар(?:С|РѕРІ|РѕРј|Р°|РЅСРµ)?|номенклаССѓСЂ|РјР°Сериал(?:С|РѕРІ|Р°|ам)?|item(?:s)?|sku|product(?:s)?)/iu.test(text);
const hasBalanceLexeme =
/(?:леР(?:РёС|Р°С)|есССЊ|СРёСЃР»(?:РёС(?:СЃСЏ|СЃСЊ)|СЏССЃСЏ)|РѕСЃСР°С(?:РѕРє|РєРё)|срез|РЅР°\s+РґР°С|РїРѕ\s+СЃРѕСЃСРѕСЏРЅРёСЋ|РЅР°\s+РєРѕРЅРµС|РїСЂРѕРёСЃСРѕРґ(?:РёС|ило|СЏСее)|СЃРёССѓР°С(?:РёСЏ|РёРё)|РѕР±СЃСановк(?:Р°|Рё)|today|now|current|as\s+of)/iu.test(
text
);
const hasRequestCue =
/(?:РїРѕРєР°РРё|показаССЊ|РІСведи|РґР°Р|какие|ССРѕ|С[РµСРѕ]|какоР|сколько|проверь|провериССЊ|Секни|check|show|list|which|what)/iu.test(
text
);
if (hasAccount41Anchor && (hasGoodsLexeme || hasBalanceLexeme || hasRequestCue || hasInventoryAsOfCue(text))) {
return true;
}
return (hasGoodsLexeme || hasBalanceLexeme || hasColloquialStockSnapshotCue || hasStockStateCue) &&
(hasRequestCue || hasBalanceLexeme || hasColloquialStockSnapshotCue || hasStockStateCue);
}
function hasInventoryProvenanceSignal(text: string): boolean {
return /(?:Р СР СстаРРС‰РСР С|Р·Р°РСРЎСР СР С|Р СРЎРР СР ССЃС…РСР ВР Т|Р СС‚РСРЎСР ТР В°|Р СР СР СР ТР В° Р В±РЎвЂР В» Р СРЎСР СлеРР|Р°РСС‚РСР РР Рая Р·Р°РСРЎСР СР С|purchase provenance|purchase date|supplier provenance|stock overlap)/iu.test(
text
);
}
function hasInventoryPurchaseDocumentsSignal(text: string): boolean {
return /(?:Р СР С Р СР°РСР СРј Р ТР СР СРЎСРјРµРРтам|Р ТР СР СРЎСРјРµРРтцзаРСРЎСР СР СР С|purchase documents|documents of purchase|through which documents|chain of documents)/iu.test(
text
);
}
function hasInventorySaleTraceSignal(text: string): boolean {
return /(?:Р СРЎРР СР ТР°РВ|Р СР СР СРЎСР Сател|buyer|sale trace|purchase[\s-]?to[\s-]?sale|purchase -> warehouse -> sale|Р·Р°РСРЎСР СР СР В°.*Р СРЎРР СР ТР°РВ)/iu.test(
text
);
}
function hasSelectedObjectInventoryCue(text: string): boolean {
return /(?:РїРѕ\s+РІСбранному\s+объекССѓ|РїРѕ\s+РІСбранноР\s+РїРѕР·РёСРёРё|РїРѕ\s+СЌСРѕР\s+РїРѕР·РёСРёРё|РїРѕ\s+СЌСРѕРјСѓ\s+Совару|РїРѕ\s+нему|РїРѕ\s+РЅРµР|РїРѕ\s+РЅРёРј|РїРѕ\s+нему\s+РРµ|РїРѕ\s+РЅРµР\s+РРµ|selected\s+object)/iu.test(
String(text ?? "")
);
}
function hasSelectedObjectInventoryProvenanceSignal(text: string): boolean {
return hasSelectedObjectInventoryCue(text) && hasInventorySupplierCue(text);
}
function hasSelectedObjectInventoryPurchaseDocumentsSignal(text: string): boolean {
const hasPurchaseDocumentsCue =
/(?:РїРѕ\s+каким\s+докуменСам\s+(?:СЌСРѕ|его|СЌСРѕС\s+Совар|СЌССѓ\s+РїРѕР·РёСРёСЋ)\s+купили|РїРѕ\s+каким\s+докуменСам\s+(?:Р±СР»\s+)?куплен|какими\s+докуменСами\s+(?:СЌСРѕ|его|СЌСРѕС\s+Совар|СЌССѓ\s+РїРѕР·РёСРёСЋ)\s+купили|какими\s+докуменСами\s+(?:Р±СР»\s+)?куплен|purchase\s+documents|documents\s+of\s+purchase|through\s+which\s+documents)/iu.test(
text
) ||
/(?:(?:РїРѕ\s+каким|какими)\s+РґРѕРє[Р°-СЏС]*[\s\S]{0,80}(?:РєСѓРїРёР»|куплен)|РґРѕРє(?:Рё|уменС[Р°-СЏС]*)[\s\S]{0,80}(?:РїРѕ\s+(?:РЅРёРј|РЅРµР|нему|СЌСРѕР\s+РїРѕР·РёСРёРё|СЌСРѕРјСѓ\s+Совару)|операСРё)|(?:РїРѕ\s+(?:РЅРёРј|РЅРµР|нему|СЌСРѕР\s+РїРѕР·РёСРёРё|СЌСРѕРјСѓ\s+Совару))[\s\S]{0,80}РґРѕРє(?:Рё|уменС[Р°-СЏС]*))/iu.test(
text
);
return (
hasSelectedObjectInventoryCue(text) &&
hasPurchaseDocumentsCue
);
}
function hasSelectedObjectInventorySaleTraceSignal(text: string): boolean {
return hasSelectedObjectInventoryCue(text) && hasInventorySaleCue(text);
}
function hasSelectedObjectInventoryProfitabilitySignal(text: string): boolean {
return hasSelectedObjectInventoryCue(text) && hasInventoryProfitabilityCue(text);
}
function hasInventoryProvenanceSignalV2(text: string): boolean {
const hasItemCue = /(?:Совар|номенклаССѓСЂ|sku|item|product|РѕСЃСР°С(?:РѕРє|РєРё)|склад)/iu.test(text);
const hasSupplierCue = hasInventorySupplierCue(text) || /кем\s+РїРѕСЃСавлен/iu.test(text);
const hasPurchaseCue =
/(?:куплен(?:С|Р°|Рѕ)?|закупк|РїСЂРѕРёСЃСРѕРдени|РѕСРєСѓРґР°|РіРґРµ\s+(?:РјС\s+)?купили(?:\s+(?:СЌСРѕ|его|Совар|РїРѕР·РёСРёСЋ))?|РіРґРµ\s+куплено|РєРѕРіРґР°\s+Р±СР»\s+куплен|РєРѕРіРґР°\s+куплен|РґР°СР°\s+закупк|РєСРѕ\s+(?:нам\s+)?РїРѕСЃСавил|кем\s+РїРѕСЃСавлен|РїРѕСЃСавлен(?:С|Р°)?|purchase\s+provenance|purchase\s+date)/iu.test(
text
) || hasInventoryPurchaseStem(text);
return hasItemCue && hasSupplierCue && hasPurchaseCue;
}
function hasInventoryPurchaseDateSignal(text: string): boolean {
const hasItemCue =
/(?:Совар|номенклаССѓСЂ|sku|item|product)/iu.test(text) || hasSelectedObjectInventoryCue(text);
const hasPurchaseDateCue =
/(?:РєРѕРіРґР°\s+(?:примерно\s+)?(?:РјС\s+)?купили|РєРѕРіРґР°\s+Р±СР»\s+куплен|РєРѕРіРґР°\s+куплен|РґР°СР°\s+закупк|purchase\s+date)/iu.test(
text
) ||
/(?:РєРѕРіРґР°\s+Р±СР»(?:Р°|Рё|Рѕ)?\s+закупк\w*|РєРѕРіРґР°\s+закупк\w*)/iu.test(text);
return hasItemCue && hasPurchaseDateCue;
}
function hasInventoryPurchaseDocumentsSignalV2(text: string): boolean {
const hasItemCue = /(?:Совар|номенклаССѓСЂ|sku|item|product)/iu.test(text);
const hasPurchaseDocCue = /(?:РїРѕ\s+каким\s+докуменСам\s+Р±СР»\s+куплен|РїРѕ\s+каким\s+докуменСам\s+куплен|какими\s+докуменСами\s+Р±СР»\s+куплен|докуменС(?:ам|С)\s+закупк|purchase\s+documents|documents\s+of\s+purchase|through\s+which\s+documents)/iu.test(
text
);
return hasItemCue && hasPurchaseDocCue;
}
function hasInventorySaleTraceSignalV2(text: string): boolean {
const hasItemCue = /(?:Совар|номенклаССѓСЂ|sku|item|product|РїРѕР·РёСРё(?:СЏ|СЋ|Рё)|РїСЂРѕРґСѓРєСРё(?:СЏ|СЋ|Рё))/iu.test(text);
const hasTraceCue =
/(?:РєРѕРјСѓ\s+(?:РІ\s+РёСРѕРіРµ\s+)?(?:РјС\s+)?продали|РєРѕРјСѓ\s+Р±СР»\s+продан|РєСѓРґР°\s+(?:РІ\s+РёСРѕРіРµ\s+)?(?:РјС\s+)?продали(?:\s+(?:СЌСРѕ|его|Совар|РїРѕР·РёСРёСЋ))?|РєСѓРґР°\s+(?:Р±Сла\s+)?реализована\s+(?:РїРѕР·РёСРёСЏ|номенклаССѓСЂР°|РїСЂРѕРґСѓРєСРёСЏ)|РєСРѕ\s+РєСѓРїРёР»|buyer|sale\s+trace|trace\s+of\s+sale|Серез\s+какие\s+докуменСС\s+РїСЂРѕС[РµС]Р»\s+РїСѓССЊ\s+Совара|закупк.*склад.*РїСЂРѕРґР°Р|purchase[\s-]?to[\s-]?sale|purchase\s*->\s*warehouse\s*->\s*sale|purchase\s*->\s*stock\s*->\s*sale)/iu.test(
text
);
return hasItemCue && hasTraceCue;
}
function hasInventorySupplierStockOverlapSignal(text: string): boolean {
const hasDirectSingleItemSupplierQuestion =
/(?:РѕС\s+какого\s+РїРѕСЃСавСРёРєР°\s+куплен\s+(?:Совар|номенклаССѓСЂ(?:Р°|Сѓ|С)|РїРѕР·РёСРё(?:СЏ|СЋ|Рё))|РѕС\s+РєРѕРіРѕ\s+куплен\s+(?:Совар|номенклаССѓСЂ(?:Р°|Сѓ|С)|РїРѕР·РёСРё(?:СЏ|СЋ|Рё)))/iu.test(
text
);
if (hasDirectSingleItemSupplierQuestion) {
return false;
}
const hasSupplierCue = /(?:РїРѕСЃСавСРёРє|supplier|vendor|РѕС\s+РїРѕСЃСавСРёРєР°|Сѓ\s+РїРѕСЃСавСРёРєР°)/iu.test(text);
const hasStockCue = /(?:склад|РѕСЃСР°С(?:РѕРє|РєРµ|РєРѕРІ)|леРР°С|леРРёС|СЃРµРСас\s+РµСРµ|СЃРµРСас\s+РµС[РµС]|РЅР°\s+РґР°ССѓ|РїРѕ\s+СЃРѕСЃСРѕСЏРЅРёСЋ\s+РЅР°\s+РґР°ССѓ|current\s+stock|stock\s+overlap|ССРѕ\s+СЃРµРСас\s+леРРёС)/iu.test(
text
);
return hasSupplierCue && hasStockCue;
}
function hasInventoryAgingSignal(text: string): boolean {
const hasResidueCue =
/(?:РѕСЃСР°С(?:РѕРє|РєРё)|РІ\s+РѕСЃСР°СРєРµ|среди\s+СекуСРёС\s+РѕСЃСР°СРєРѕРІ|РЅР°\s+складе|stock\s+residue|stock\s+balance)/iu.test(text);
const hasAgingCue =
/(?:СЃСар(?:СРµ|СРј|СС)\s+закупк|СЃСарС(?:Рј|С)\s+закупк(?:ам|Рё|Р°С)|РѕСРЅРѕСЃРёС(?:СЃСЏ|СЃСЏ\s+ли)?\s+.*\s+Рє\s+СЃСарСРј\s+закупк|закупал(?:РёСЃСЊ|СЃСЏ)\s+РѕСень\s+давно|РѕСень\s+давно|давно\s+куплен|давно\s+РїСЂРёРѕР±СЂРµСен|куплен\s+задолго\s+РґРѕ(?:\s+РґР°СС)?|закуплен(?:С|Р°)?\s+давно|РїСЂРёРѕР±СЂРµСен\s+давно|задолго\s+РґРѕ(?:\s+РґР°СС)?|возрасС\s+РѕСЃСР°СРє|возрасС\s+закупк|aged?\s+stock|old\s+purchase|old\s+purchases|old\s+stock|bought\s+long\s+ago|purchased\s+long\s+ago|aging\s+by\s+purchase\s+date|very\s+old\s+stock|very\s+old\s+purchase|old\s+procurement|older\s+purchases|aged\s+items|old\s+goods)/iu.test(
text
);
return hasAgingCue || (hasResidueCue && /(?:давно\s+куплен|давно\s+РїСЂРёРѕР±СЂРµСен|задолго\s+РґРѕ)/iu.test(text));
}
function hasInventoryPurchaseToSaleChainSignal(text: string): boolean {
const hasItemCue = /(?:Совар|номенклаССѓСЂ|sku|item|product)/iu.test(text);
const hasChainCue =
/(?:закупк.*склад.*РїСЂРѕРґР°Р|purchase[\s-]?to[\s-]?sale|purchase\s*->\s*(?:warehouse|stock)\s*->\s*sale|закупка\s*->\s*склад\s*->\s*РїСЂРѕРґР°РР°|СепоСРє[аи]\s+РґРІРёРен|докуменСально\s+РїРѕРґСверРденн\w+\s+СепоСРє|supplier\s*->\s*item\s*->\s*(?:buyer|customer)|supplier\s+to\s+buyer|supplier\s+to\s+item\s+to\s+buyer)/iu.test(
text
) || text.includes("->");
return hasItemCue && hasChainCue;
}
function hasInventorySupplierToBuyerChainSignal(text: string): boolean {
const hasSupplierCue = /(?:РїРѕСЃСавСРёРє|supplier|vendor)/iu.test(text);
const hasBuyerCue = /(?:РїРѕРєСѓРїР°Сел|buyer|customer|client)/iu.test(text);
const hasItemCue = /(?:Совар|номенклаССѓСЂ|sku|item|product)/iu.test(text);
const hasChainCue =
/(?:докуменСально\s+РїРѕРґСверРденн\w+\s+СепоСРє|supplier\s*->\s*item\s*->\s*buyer|supplier\s*->\s*item\s*->\s*customer|supplier\s*->\s*buyer|supplier\s+to\s+buyer|supplier\s+to\s+buyer\s+chain|supplier\s+to\s+item\s+to\s+buyer|РїРѕСЃСавСРёРє\s*->\s*Совар\s*->\s*РїРѕРєСѓРїР°Сел|РїРѕСЃСавСРёРє\s*->\s*Совар\s*->\s*клиенС|РїРѕСЃСавСРёРє\s*->\s*Совар\s*->\s*РїРѕРєСѓРїР°Сель|РїРѕСЃСавСРёРє\s+Рє\s+РїРѕРєСѓРїР°Сел|РїРѕСЃСавСРёРє\s+Рє\s+клиенС|РїРѕСЃСавСРёРє\s+Рє\s+Совару\s+Рё\s+РїРѕРєСѓРїР°Селю)/iu.test(
text
) || text.includes("->");
return hasSupplierCue && hasBuyerCue && hasItemCue && hasChainCue;
}
function hasCustomerRevenueRankingBridgeSignal(text: string): boolean {
const normalized = String(text ?? "").trim().toLowerCase();
if (!normalized) {
return false;
}
const hasSupplierCue =
/(?:\u043f\u043e\u0441\u0442\u0430\u0432\u0449\u0438\u043a|\u0432\u0435\u043d\u0434\u043e\u0440|supplier|vendor)/iu.test(
normalized
);
if (hasSupplierCue) {
return false;
}
const hasDirectRankingCue =
/(?:\u0441\u0430\u043c(?:\u044b\u0439|\u0430\u044f|\u043e\u0435)?\s+\u0434\u043e\u0445\u043e\u0434\u043d(?:\u044b\u0439|\u0430\u044f|\u043e\u0435|\u044b\u0435|\u043e\u0433\u043e|\u043e\u043c\u0443|\u044b\u043c|\u044b\u0445)?\s+(?:\u043a\u043b\u0438\u0435\u043d\u0442|customer|client)|(?:\u043a\u0430\u043a\u043e\u0439|\u043a\u0430\u043a\u0430\u044f)\s+(?:\u0443\s+\u043d\u0430\u0441\s+)?\u0441\u0430\u043c(?:\u044b\u0439|\u0430\u044f|\u043e\u0435)?\s+\u0434\u043e\u0445\u043e\u0434\u043d(?:\u044b\u0439|\u0430\u044f|\u043e\u0435|\u044b\u0435|\u043e\u0433\u043e|\u043e\u043c\u0443|\u044b\u043c|\u044b\u0445)?\s+\u0433\u043e\u0434|(?:\u0430\s+)?(?:\u0441\u043a\u043e\u043b\u044c\u043a\u043e|\u0441\u043a\u043e\u043a)\s+(?:\u0432\u043e\u043e\u0431\u0449\u0435\s+)?(?:\u0434\u0435\u043d\u0435\u0433\s+)?\u043c\u044b\s+\u0437\u0430\u0440\u0430\u0431\u043e\u0442\u0430\u043b\u0438(?:\s+\u0437\u0430\s+\u0432\u0441\u0435\s+\u0432\u0440\u0435\u043c\u044f)?|(?:\u0430\s+)?(?:\u0437\u0430|for)\s+\d{4}\s+\u043c\u044b\s+(?:\u0441\u043a\u043e\u043b\u044c\u043a\u043e|\u0441\u043a\u043e\u043a)\s+\u0437\u0430\u0440\u0430\u0431\u043e\u0442\u0430\u043b\u0438|(?:\u0441\u043a\u043e\u043b\u044c\u043a\u043e|\u0441\u043a\u043e\u043a)\s+\u043c\u044b\s+\u0437\u0430\u0440\u0430\u0431\u043e\u0442\u0430\u043b\u0438\s+\u0437\u0430\s+\d{4}|(?:\u043e\u0431\u0449\u0430\u044f\s+)?\u0432\u044b\u0440\u0443\u0447\u043a\u0430\s+\u0437\u0430\s+\d{4})/iu.test(
normalized
);
if (hasDirectRankingCue) {
return true;
}
const hasMoneyCue =
/(?:\u0434\u0435\u043d\u044c\u0433|\u0434\u0435\u043d\u0435\u0433|\u0432\u044b\u0440\u0443\u0447|\u0434\u043e\u0445\u043e\u0434|\u043e\u0431\u043e\u0440\u043e\u0442|revenue|turnover|money|inflow)/iu.test(
normalized
);
if (!hasMoneyCue) {
return false;
}
const hasCustomerRankingCue =
/(?:\u043a\u0442\u043e\s+(?:\u043d\u0430\u043c\s+)?(?:\u0431\u043e\u043b\u044c\u0448\u0435(?:\s+\u0432\u0441\u0435\u0433\u043e)?\s+\u043f\u0440\u0438\u043d\u0435\u0441(?:\s+\u0434\u0435\u043d\u0435\u0433)?|\u043f\u0440\u0438\u043d\u0435\u0441\s+\u0431\u043e\u043b\u044c\u0448\u0435(?:\s+\u0432\u0441\u0435\u0433\u043e)?\s+\u0434\u0435\u043d\u0435\u0433)|who\s+brought\s+(?:us\s+)?(?:the\s+)?most\s+money|(?:\u0441\u0430\u043c(?:\u044b\u0439|\u0430\u044f|\u043e\u0435)?\s+\u0434\u043e\u0445\u043e\u0434\u043d\w*\s+(?:\u043a\u043b\u0438\u0435\u043d\u0442|customer|client)))/iu.test(
normalized
);
const hasRevenueAggregateCue =
/(?:(?:\u043a\u0430\u043a\u043e\u0439|\u043a\u0430\u043a\u0430\u044f)\s+(?:\u0443\s+\u043d\u0430\u0441\s+)?\u0441\u0430\u043c(?:\u044b\u0439|\u0430\u044f|\u043e\u0435)?\s+\u0434\u043e\u0445\u043e\u0434\u043d\w*\s+\u0433\u043e\u0434|(?:\u0441\u043a\u043e\u043b\u044c\u043a\u043e|скок)\s+(?:\u0432\u043e\u043e\u0431\u0449\u0435\s+)?(?:\u0434\u0435\u043d\u0435\u0433\s+)?\u043c\u044b\s+\u0437\u0430\u0440\u0430\u0431\u043e\u0442\u0430\u043b\u0438|(?:\u0437\u0430|for)\s+\d{4}\s+\u043c\u044b\s+(?:\u0441\u043a\u043e\u043b\u044c\u043a\u043e|скок)\s+\u0437\u0430\u0440\u0430\u0431\u043e\u0442\u0430\u043b\u0438|\u0432\u044b\u0440\u0443\u0447\u043a\w*\s+\u0437\u0430\s+\d{4})/iu.test(
normalized
);
return hasCustomerRankingCue || hasRevenueAggregateCue;
}
function hasSpecificCounterpartyRevenueBridgeSignal(text: string): boolean {
const normalized = String(text ?? "").trim().toLowerCase();
if (!normalized) {
return false;
}
const hasSupplierCue =
/(?:\u043f\u043e\u0441\u0442\u0430\u0432\u0449\u0438\u043a|\u0432\u0435\u043d\u0434\u043e\u0440|supplier|vendor)/iu.test(
normalized
);
const hasNonRevenueEntityCue =
/(?:\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442|\u0434\u043e\u043a\u0438|\u0434\u043e\u0433\u043e\u0432\u043e\u0440|\u043a\u043e\u043d\u0442\u0440\u0430\u043a\u0442|\u0431\u0430\u043d\u043a|\u043f\u043b\u0430\u0442\u0435\u0436|docs?|documents?|contract|bank|payment)/iu.test(
normalized
);
if (hasSupplierCue || hasNonRevenueEntityCue) {
return false;
}
const hasRevenueCue =
/(?:\u043e\u0431\u043e\u0440\u043e\u0442|\u0432\u044b\u0440\u0443\u0447\w*|\u0434\u043e\u0445\u043e\u0434\w*|revenue|turnover)/iu.test(
normalized
);
if (!hasRevenueCue) {
return false;
}
const explicitEntityMatch =
normalized.match(/(?:^|[\s,.;:!?])(?:\u043f\u043e|by|for)\s+([\p{L}\d][\p{L}\d._-]{1,})\s*$/iu) ??
normalized.match(
/(?:\u043e\u0431\u043e\u0440\u043e\u0442|\u0432\u044b\u0440\u0443\u0447\w*|\u0434\u043e\u0445\u043e\u0434\w*|revenue|turnover)\s+(?:(?:\u0431\u044b\u043b(?:\u0430|\u043e)?|was)\s+)?(?:(?:\u0443|\u043f\u043e|by|for)\s+)?([\p{L}\d][\p{L}\d._-]{1,})\s*$/iu
);
const entity = explicitEntityMatch?.[1] ? String(explicitEntityMatch[1]).toLowerCase() : null;
if (!entity || /^\d+$/.test(entity)) {
return false;
}
const ignoredEntityTails = new Set([
"\u043d\u0430\u043c",
"\u043d\u0430\u0441",
"\u0432\u0441\u0435",
"\u0432\u0441\u0435\u043c",
"\u0433\u043e\u0434",
"\u0433\u043e\u0434\u0430",
"\u043c\u0435\u0441\u044f\u0446",
"year",
"month"
]);
return !ignoredEntityTails.has(entity);
}
function hasInventoryProvenanceBridgeSignal(text: string): boolean {
const normalized = String(text ?? "").trim().toLowerCase();
if (!normalized) {
return false;
}
const hasItemCue =
/(?:\u0442\u043e\u0432\u0430\u0440|\u043f\u043e\u0437\u0438\u0446|\u043d\u043e\u043c\u0435\u043d\u043a\u043b\u0430\u0442\u0443\u0440|sku|item|product)/iu.test(
normalized
);
const hasSupplierCue =
/(?:\u043f\u043e\u0441\u0442\u0430\u0432\u0449\u0438\u043a|\u043e\u0442\s+\u043a\u0430\u043a\u043e\u0433\u043e|\u043a\u0442\u043e\s+\u043f\u043e\u0441\u0442\u0430\u0432\u0438\u043b|supplier|vendor)/iu.test(
normalized
);
const hasPurchaseCue =
/(?:\u043a\u0443\u043f\u043b\u0435\u043d|\u0437\u0430\u043a\u0443\u043f|\u043a\u043e\u0433\u0434\u0430\s+\u043a\u0443\u043f\u0438\u043b|\u043a\u0443\u043f\u0438\u043b\u0438|purchase)/iu.test(
normalized
);
return hasItemCue && hasSupplierCue && hasPurchaseCue;
}
function hasInventoryDocumentaryChainBridgeSignal(text: string): boolean {
const normalized = String(text ?? "").trim().toLowerCase();
if (!normalized) {
return false;
}
const hasChainCue =
/(?:\u0446\u0435\u043f\u043e\u0447|\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u043b|\u043f\u043e\u0434\u0442\u0432\u0435\u0440\u0436\u0434|->|\u2192|chain|trace)/iu.test(
normalized
);
const hasSupplierCue = /(?:\u043f\u043e\u0441\u0442\u0430\u0432\u0449\u0438\u043a|supplier|vendor)/iu.test(normalized);
const hasBuyerCue = /(?:\u043f\u043e\u043a\u0443\u043f\u0430\u0442\u0435\u043b|\u043a\u043b\u0438\u0435\u043d\u0442|buyer|customer|client)/iu.test(normalized);
const hasItemCue =
/(?:\u0442\u043e\u0432\u0430\u0440|\u043f\u043e\u0437\u0438\u0446|\u043d\u043e\u043c\u0435\u043d\u043a\u043b\u0430\u0442\u0443\u0440|sku|item|product)/iu.test(
normalized
);
return hasChainCue && hasSupplierCue && hasBuyerCue && hasItemCue;
}
function hasColloquialInventoryOnHandBridgeSignal(text: string): boolean {
const normalized = String(text ?? "").trim().toLowerCase();
if (!normalized) {
return false;
}
const hasInventoryAgingCue =
/(?:\u043e\u0447\u0435\u043d\u044c\s+\u0434\u0430\u0432\u043d\u043e|\u0434\u0430\u0432\u043d\u043e\s+\u043a\u0443\u043f\u043b|\u0434\u0430\u0432\u043d\u043e\s+\u043f\u0440\u0438\u043e\u0431\u0440\u0435\u0442|\u0441\u0442\u0430\u0440(?:\u044b\u0435|\u044b\u043c|\u044b\u0445)?\s+\u0437\u0430\u043a\u0443\u043f|\u0441\u0442\u0430\u0440\u044b\u0439\s+\u0442\u043e\u0432\u0430\u0440|old\s+stock|old\s+purchase|aging\s+by\s+purchase\s+date)/iu.test(
normalized
);
if (hasInventoryAgingCue) {
return false;
}
const tokenCount = normalized.split(/\s+/u).filter(Boolean).length;
const hasWarehouseCue =
/(?:\u0441\u043a\u043b\u0430\u0434(?:\u0430\u0445|\u0435|\u0443|\u043e\u043c|\u044b)?|\u043e\u0441\u0442\u0430\u0442|warehouse|stock|inventory)/iu.test(
normalized
);
if (!hasWarehouseCue) {
return false;
}
const hasQuestionCue =
/(?:\u0447\u0442\u043e|\u0447\u0435|\u0447\u0451|\u043a\u0430\u043a\u0438\u0435|\u043f\u043e\u043a\u0430\u0436\u0438|\u043f\u043e\u043a\u0430\u0437\u0430\u0442\u044c|show|list|what)/iu.test(
normalized
);
return hasQuestionCue && tokenCount <= 8;
}
function repairLikelyUtf8Mojibake(text: string): string {
const raw = String(text ?? "");
if (!raw) {
return "";
}
try {
const repaired = Buffer.from(raw, "latin1").toString("utf8");
return repaired || raw;
} catch {
return raw;
}
}
export function resolveAddressIntent(userMessage: string): AddressIntentResolution {
const text = String(userMessage ?? "").trim().toLowerCase();
const repairedText = repairLikelyUtf8Mojibake(text).trim().toLowerCase();
const bridgeText = repairedText && repairedText !== text ? `${text} ${repairedText}` : text;
const turnNoiseNormalizedBridgeText = bridgeText
.replace(/(^|[^\p{L}0-9_])\u043d\u0430\u043c\u0441(?=$|[^\p{L}0-9_])/giu, "$1\u043d\u0430\u043c")
.replace(/(^|[^\p{L}0-9_])\u043a\u0430\u043a\u0438\u0435\u043a(?=$|[^\p{L}0-9_])/giu, "$1\u043a\u0430\u043a\u0438\u0435");
const currentTurnBridgeText =
turnNoiseNormalizedBridgeText !== bridgeText ? `${bridgeText} ${turnNoiseNormalizedBridgeText}` : bridgeText;
const hasLooseVatPayableBridge =
/(?:\u043d\u0434\u0441|vat)/iu.test(text) &&
/(?:\u043a\u0430\u043a\u043e\u0439\s+\u043d\u0434\u0441\s+(?:(?:\u043d\u0430\u043c|(?:\u043c\u044b\s+)?\u0434\u043e\u043b\u0436\u043d\u044b)\s+)?(?:\u043d\u0430\u0434\u043e|\u043d\u0443\u0436\u043d\u043e|\u043d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u043e)|(?:\u043d\u0430\u043c|\u043c\u044b\s+)?\u043d\u0430\u0434\u043e\s+(?:\u0437\u0430\u043f\u043b\u0430\u0442\u0438\u0442\u044c|\u0441\u0433\u0440\u0443\u0437\u0438\u0442\u044c)|(?:\u043d\u0430\u043c|\u043c\u044b\s+)?\u043d\u0443\u0436\u043d\u043e\s+(?:\u0437\u0430\u043f\u043b\u0430\u0442\u0438\u0442\u044c|\u0441\u0433\u0440\u0443\u0437\u0438\u0442\u044c)|\u043c\u044b\s+\u0434\u043e\u043b\u0436\u043d\u044b\s+(?:\u0437\u0430\u043f\u043b\u0430\u0442\u0438\u0442\u044c|\u0441\u0433\u0440\u0443\u0437\u0438\u0442\u044c)|\u043d\u0434\u0441\s+\u043a\s+\u0443\u043f\u043b\u0430\u0442\u0435)/iu.test(
text
) &&
/(?:\u0437\u0430\s+(?:\d{4}|(?:\u044f\u043d\u0432\u0430\u0440|\u0444\u0435\u0432\u0440\u0430\u043b|\u043c\u0430\u0440\u0442|\u0430\u043f\u0440\u0435\u043b|\u043c\u0430[\u0439\u044f]|\u0438\u044e\u043d|\u0438\u044e\u043b|\u0430\u0432\u0433\u0443\u0441\u0442|\u0441\u0435\u043d\u0442\u044f\u0431\u0440|\u043e\u043a\u0442\u044f\u0431\u0440|\u043d\u043e\u044f\u0431\u0440|\u0434\u0435\u043a\u0430\u0431\u0440)\S*(?:\s+(?:19|20)\d{2})?)|\u043d\u0430\s+(?:\u044f\u043d\u0432\u0430\u0440|\u0444\u0435\u0432\u0440\u0430\u043b|\u043c\u0430\u0440\u0442|\u0430\u043f\u0440\u0435\u043b|\u043c\u0430[\u0439\u044f]|\u0438\u044e\u043d|\u0438\u044e\u043b|\u0430\u0432\u0433\u0443\u0441\u0442|\u0441\u0435\u043d\u0442\u044f\u0431\u0440|\u043e\u043a\u0442\u044f\u0431\u0440|\u043d\u043e\u044f\u0431\u0440|\u0434\u0435\u043a\u0430\u0431\u0440)\S*(?:\s+(?:19|20)\d{2})?|\u0432\s+(?:\u044f\u043d\u0432\u0430\u0440|\u0444\u0435\u0432\u0440\u0430\u043b|\u043c\u0430\u0440\u0442|\u0430\u043f\u0440\u0435\u043b|\u043c\u0430[\u0439\u044f]|\u0438\u044e\u043d|\u0438\u044e\u043b|\u0430\u0432\u0433\u0443\u0441\u0442|\u0441\u0435\u043d\u0442\u044f\u0431\u0440|\u043e\u043a\u0442\u044f\u0431\u0440|\u043d\u043e\u044f\u0431\u0440|\u0434\u0435\u043a\u0430\u0431\u0440)\S*(?:\s+(?:19|20)\d{2})?|\b[1-4]\s*(?:\u043a\u0432\u0430\u0440\u0442\u0430\u043b|\u043a\u0432\.?)\b)/iu.test(
text
);
if (hasLooseVatPayableBridge) {
return {
intent: "vat_liability_confirmed_for_tax_period",
confidence: "high",
reasons: ["vat_liability_colloquial_bridge_signal_detected"]
};
}
const hasExplicitReceivablesSnapshotBridge =
/(?:\u043d\u0430\u043c\s+\u043a\u0442\u043e-\u0442\u043e\s+\u0434\u043e\u043b\u0436\u0435\u043d|\u043d\u0430\u043c\s+\u043a\u0442\u043e\s+\u0434\u043e\u043b\u0436\u0435\u043d|\u043a\u0442\u043e\s+\u043d\u0430\u043c\s+\u0434\u043e\u043b\u0436\u0435\u043d|\u0435\u0441\u0442\u044c\s+\u043b\u0438\s+\u0434\u0435\u0431\u0438\u0442\u043e\u0440\u0441\u043a\w+\s+\u0437\u0430\u0434\u043e\u043b\u0436\u0435\u043d\u043d\u043e\u0441\u0442\u044c|\u0437\u0430\u0434\u043e\u043b\u0436\u0435\u043d\u043d\u043e\u0441\u0442\u044c\s+\u043d\u0430\u043c|receivables?)/iu.test(
currentTurnBridgeText
);
if (hasExplicitReceivablesSnapshotBridge) {
return {
intent: "receivables_confirmed_as_of_date",
confidence: "high",
reasons:
currentTurnBridgeText !== bridgeText
? ["receivables_snapshot_bridge_signal_detected", "current_turn_noise_normalized"]
: ["receivables_snapshot_bridge_signal_detected"]
};
}
const hasExplicitPayablesSnapshotBridge =
/(?:\u043c\u044b\s+\u0434\u043e\u043b\u0436\u043d\u044b\s+\u043a\u043e\u043c\u0443-\u0442\u043e\s+\u0434\u0435\u043d\u0435\u0433|\u043c\u044b\s+\u0434\u043e\u043b\u0436\u043d\u044b\s+\u043a\u043e\u043c\u0443\u0442\u043e\s+\u0434\u0435\u043d\u0435\u0433|\u043c\u044b\s+\u043a\u043e\u043c\u0443-\u0442\u043e\s+\u0434\u043e\u043b\u0436\u043d\u044b|\u043a\u043e\u043c\u0443\s+\u043c\u044b\s+\u0434\u043e\u043b\u0436\u043d\u044b|\u0435\u0441\u0442\u044c\s+\u043b\u0438\s+\u0437\u0430\u0434\u043e\u043b\u0436\u0435\u043d\u043d\u043e\u0441\u0442\u044c\s+\u043f\u0435\u0440\u0435\u0434\s+\u043a\u043e\u043d\u0442\u0440\u0430\u0433\u0435\u043d\u0442\u0430\u043c\u0438|\u0437\u0430\u0434\u043e\u043b\u0436\u0435\u043d\u043d\u043e\u0441\u0442\u044c\s+\u043f\u0435\u0440\u0435\u0434\s+\u043a\u043e\u043d\u0442\u0440\u0430\u0433\u0435\u043d\u0442\u0430\u043c\u0438|payables?)/iu.test(
currentTurnBridgeText
);
if (hasExplicitPayablesSnapshotBridge) {
return {
intent: "payables_confirmed_as_of_date",
confidence: "high",
reasons:
currentTurnBridgeText !== bridgeText
? ["payables_snapshot_bridge_signal_detected", "current_turn_noise_normalized"]
: ["payables_snapshot_bridge_signal_detected"]
};
}
const hasDirectInventoryAgingBridge =
/(?:\u043e\u0447\u0435\u043d\u044c\s+\u0434\u0430\u0432\u043d\u043e|\u0434\u0430\u0432\u043d\u043e\s+\u043a\u0443\u043f\u043b|\u0434\u0430\u0432\u043d\u043e\s+\u043f\u0440\u0438\u043e\u0431\u0440\u0435\u0442|\u0441\u0442\u0430\u0440(?:\u044b\u0435|\u044b\u043c|\u044b\u0445)?\s+\u0437\u0430\u043a\u0443\u043f|\u0441\u0442\u0430\u0440(?:\u044b\u0439|\u0430\u044f|\u043e\u0435)?\s+\u0442\u043e\u0432\u0430\u0440|old\s+stock|old\s+purchase|very\s+old\s+stock|aging\s+by\s+purchase\s+date)/iu.test(
bridgeText
);
if (hasDirectInventoryAgingBridge || hasInventoryAgingSignal(text) || hasInventoryAgingSignal(repairedText)) {
return {
intent: "inventory_aging_by_purchase_date",
confidence: "high",
reasons: ["inventory_aging_bridge_signal_detected"]
};
}
const hasDirectRevenueAggregateBridge =
/(?:\u0441\u0430\u043c(?:\u044b\u0439|\u0430\u044f|\u043e\u0435)?\s+\u0434\u043e\u0445\u043e\u0434\u043d(?:\u044b\u0439|\u0430\u044f|\u043e\u0435|\u044b\u0435|\u043e\u0433\u043e|\u043e\u043c\u0443|\u044b\u043c|\u044b\u0445)?\s+\u043a\u043b\u0438\u0435\u043d\u0442|(?:\u043a\u0430\u043a\u043e\u0439|\u043a\u0430\u043a\u0430\u044f)\s+(?:\u0443\s+\u043d\u0430\u0441\s+)?\u0441\u0430\u043c(?:\u044b\u0439|\u0430\u044f|\u043e\u0435)?\s+\u0434\u043e\u0445\u043e\u0434\u043d(?:\u044b\u0439|\u0430\u044f|\u043e\u0435|\u044b\u0435|\u043e\u0433\u043e|\u043e\u043c\u0443|\u044b\u043c|\u044b\u0445)?\s+\u0433\u043e\u0434|(?:\u0441\u043a\u043e\u043b\u044c\u043a\u043e|\u0441\u043a\u043e\u043a).*(?:\u0437\u0430\u0440\u0430\u0431\u043e\u0442\u0430\u043b\u0438|\u0432\u044b\u0440\u0443\u0447\u0438\u043b\u0438)|\u0432\u044b\u0440\u0443\u0447\u043a\u0430\s+\u0437\u0430\s+\d{4})/iu.test(
bridgeText
);
const hasSpecificCounterpartyRevenueBridge = [text, repairedText, turnNoiseNormalizedBridgeText, currentTurnBridgeText].some(
(sample) => hasSpecificCounterpartyRevenueBridgeSignal(sample)
);
if (
hasDirectRevenueAggregateBridge ||
hasCustomerRevenueRankingBridgeSignal(bridgeText) ||
hasSpecificCounterpartyRevenueBridge
) {
return {
intent: "customer_revenue_and_payments",
confidence: "medium",
reasons: [
hasSpecificCounterpartyRevenueBridge
? "specific_counterparty_revenue_bridge_signal_detected"
: "customer_revenue_ranking_bridge_signal_detected"
]
};
}
const hasHistoricalInventorySnapshotBridge = [text, repairedText, bridgeText].some(
(sample) =>
/(?:\u043e\u0441\u0442\u0430\u0442|inventory|stock|\u0441\u043a\u043b\u0430\u0434|РѕСЃСР°С|склад)/iu.test(sample) &&
/(?:(?:\u043d\u0430|\u0437\u0430|РЅР°|Р·Р°)\s+(?:\u044f\u043d\u0432\u0430\u0440|\u0444\u0435\u0432\u0440\u0430\u043b|\u043c\u0430\u0440\u0442|\u0430\u043f\u0440\u0435\u043b|\u043c\u0430[\u0439\u044f]|\u0438\u044e\u043d|\u0438\u044e\u043b|\u0430\u0432\u0433\u0443\u0441\u0442|\u0441\u0435\u043d\u0442\u044f\u0431\u0440|\u043e\u043a\u0442\u044f\u0431\u0440|\u043d\u043e\u044f\u0431\u0440|\u0434\u0435\u043a\u0430\u0431\u0440|марС|апрел|РјР°Р|РёСЋРЅ|РёСЋР»|авгусС|сенССЏР±СЂ|РѕРєССЏР±СЂ|РЅРѕСЏР±СЂ|декабр)\S*(?:\s+(?:19|20)\d{2})?|(?:\u043d\u0430|\u0437\u0430|РЅР°|Р·Р°)\s+\d{4}|\b(?:19|20)\d{2}\b)/iu.test(sample)
);
if (hasHistoricalInventorySnapshotBridge) {
return {
intent: "inventory_on_hand_as_of_date",
confidence: "medium",
reasons: ["inventory_historical_snapshot_bridge_signal_detected"]
};
}
if (hasInventoryDocumentaryChainBridgeSignal(text)) {
return {
intent: "inventory_purchase_to_sale_chain",
confidence: "medium",
reasons: ["inventory_documentary_chain_bridge_signal_detected"]
};
}
if (hasInventoryProvenanceBridgeSignal(text)) {
return {
intent: "inventory_purchase_provenance_for_item",
confidence: "medium",
reasons: ["inventory_provenance_bridge_signal_detected"]
};
}
if (hasColloquialInventoryOnHandBridgeSignal(text)) {
return {
intent: "inventory_on_hand_as_of_date",
confidence: "medium",
reasons: ["inventory_on_hand_colloquial_bridge_signal_detected"]
};
}
if (hasVatLiabilityConfirmedTaxPeriodSignal(text)) {
return {
intent: "vat_liability_confirmed_for_tax_period",
confidence: "high",
reasons: ["vat_liability_confirmed_tax_period_signal_detected"]
};
}
if (hasForecastTaxSignal(text)) {
return {
intent: "vat_payable_forecast",
confidence: "high",
reasons: ["forecast_tax_signal_detected"]
};
}
if (hasVatPayableConfirmedSignal(text)) {
return {
intent: "vat_payable_confirmed_as_of_date",
confidence: "high",
reasons: ["vat_payable_confirmed_signal_detected"]
};
}
if (hasAny(text, RECEIVABLES_STRONG) || hasFlexibleReceivablesDebtSignal(text)) {
const receivablesDebtLifecycleSignal =
hasReceivablesDebtLifecycleSignal(text) || hasFlexibleReceivablesDebtSignal(text);
const reasons = ["receivables_signal_detected"];
if (receivablesDebtLifecycleSignal) {
reasons.push("receivables_debt_lifecycle_signal_detected");
if (hasFlexibleReceivablesDebtSignal(text)) {
reasons.push("receivables_signal_detected_flexible_phrase");
}
}
return {
intent: receivablesDebtLifecycleSignal ? "receivables_confirmed_as_of_date" : "list_receivables_counterparties",
confidence: "high",
reasons
};
}
if (hasAny(text, PAYABLES_STRONG) || hasFlexiblePayablesDebtSignal(text)) {
const reasons = ["payables_signal_detected"];
const payablesDebtLifecycleSignal =
hasPayablesDebtLifecycleSignal(text) || hasFlexiblePayablesDebtSignal(text);
if (payablesDebtLifecycleSignal) {
reasons.push("payables_debt_lifecycle_signal_detected");
if (hasFlexiblePayablesDebtSignal(text)) {
reasons.push("payables_signal_detected_flexible_phrase");
}
}
return {
intent: payablesDebtLifecycleSignal ? "payables_confirmed_as_of_date" : "list_payables_counterparties",
confidence: "high",
reasons
};
}
if (hasSettlementGapSignal(text)) {
return {
intent: "list_open_contracts",
confidence: "medium",
reasons: ["settlement_gap_signal_detected"]
};
}
if (hasReconciliationMismatchSignal(text)) {
return {
intent: "list_open_contracts",
confidence: "medium",
reasons: ["reconciliation_mismatch_signal_detected"]
};
}
if (hasReceivablesLatencyRiskSignal(text)) {
return {
intent: "list_receivables_counterparties",
confidence: "medium",
reasons: ["receivables_payment_lag_signal_detected"]
};
}
if (hasCounterpartyDebtLongevitySignal(text)) {
return {
intent: "list_receivables_counterparties",
confidence: "medium",
reasons: ["receivables_debt_lifecycle_signal_detected"]
};
}
if (hasSupplierTailRiskSignal(text)) {
return {
intent: "list_payables_counterparties",
confidence: "medium",
reasons: ["supplier_tail_risk_signal_detected", "payables_debt_lifecycle_signal_detected"]
};
}
if (hasDocumentsFormingBalanceSignal(text) && hasDocumentsFormingBalanceAccountAnchor(text)) {
return {
intent: "documents_forming_balance",
confidence: "high",
reasons: ["documents_forming_balance_signal_detected"]
};
}
if (hasDocumentsByAccountDrilldownSignal(text)) {
return {
intent: "documents_forming_balance",
confidence: "medium",
reasons: ["documents_by_account_drilldown_signal_detected"]
};
}
const inventoryIntent = resolveInventoryAddressIntent(text);
if (inventoryIntent) {
return inventoryIntent;
}
const counterpartyIntent = resolveCounterpartyAddressIntent(text, {
hasAny,
openItemsHints: OPEN_ITEMS_HINTS,
openContractsHints: OPEN_CONTRACTS_HINTS,
documentsByCounterpartyHints: DOCUMENTS_BY_COUNTERPARTY_HINTS,
bankOperationsByCounterpartyHints: BANK_OPERATIONS_BY_COUNTERPARTY_HINTS,
documentsByContractHints: DOCUMENTS_BY_CONTRACT_HINTS,
hasCounterpartyDebtLongevitySignal,
hasInventoryAgingSignal,
hasInventoryProvenanceSignalV2,
hasInventoryPurchaseDocumentsSignalV2,
hasInventorySaleTraceSignalV2,
hasAccountNumberAnchor,
hasCompactAccountCodeToken,
hasPeriodCoverageProfileSignal,
hasPartyAnchorMention,
hasContractAnchorSignal,
hasAccountBalanceSignal,
hasDocumentTypeAndAccountSectionProfileSignal,
hasCounterpartyPopulationAndRolesSignal,
hasCounterpartyActivityLifecycleSignal,
hasContractUsageOverviewSignal,
hasOpenContractsListSignal,
hasCustomerRevenueAndPaymentsSignal,
hasSupplierPayoutsProfileSignal,
hasContractUsageAndValueSignal,
hasContractListByCounterpartySignal,
hasBankOperationSignal,
hasDocumentSignal,
hasLooseByAnchorMention,
hasHeuristicCounterpartyAnchor,
hasCounterpartyShipmentItemFlowSignal,
hasImplicitCounterpartyAnchorAroundDocs,
hasGenericAddressLookupSignal
});
if (counterpartyIntent) {
return counterpartyIntent;
}
return {
intent: "unknown",
confidence: "low",
reasons: ["intent_not_supported_in_v1"]
};
}