Post-F: закрыть ручные провалы НДС, выручки и item-flow

This commit is contained in:
2026-04-24 20:36:34 +03:00
parent 7262603a26
commit 739e8b808f
16 changed files with 695 additions and 37 deletions
@@ -864,7 +864,9 @@ function extractShipmentCounterpartyValue(text) {
return candidate;
}
function extractInstrumentalCounterpartyValue(text) {
const match = String(text ?? "").match(/(?:контрагентом|поставщиком|клиентом|заказчиком)\s+([\p{L}][\p{L}\p{N}._-]{1,})(?=[\s,.;:!?)]|$)/iu);
const source = String(text ?? "");
const match = source.match(/(?:\u043a\u043e\u043d\u0442\u0440\u0430\u0433\u0435\u043d\u0442\u043e\u043c|\u043f\u043e\u0441\u0442\u0430\u0432\u0449\u0438\u043a\u043e\u043c|\u043a\u043b\u0438\u0435\u043d\u0442\u043e\u043c|\u0437\u0430\u043a\u0430\u0437\u0447\u0438\u043a\u043e\u043c)\s+([\p{L}][\p{L}\p{N}._-]{1,})(?=[\s,.;:!?)]|$)/iu) ??
source.match(/(?:контрагентом|поставщиком|клиентом|заказчиком)\s+([\p{L}][\p{L}\p{N}._-]{1,})(?=[\s,.;:!?)]|$)/iu);
if (!match) {
return undefined;
}
@@ -1603,6 +1603,23 @@ function resolveUnicodeAddressIntentBridge(text) {
]).has(byAnchorToken);
const hasMoneyCue = /(?:деньг|денег|выручк|доход|оборот|заработ|прин[её]с|чек|ликвидн|revenue|turnover|money)/iu.test(normalized);
const hasRankingCue = /(?:топ|ранк|сам(?:ый|ая|ое|ые)|больше\s+всего|наибольш|крупн|жирн|max|top|rank)/iu.test(normalized);
const hasOpenItemsAccountCue = /(?:хвост|долг|незакрыт|вис)/iu.test(normalized) &&
/(?:сч(?:е|ё)т(?:а|у|ом|е|ов)?\s*(?:№|#)?\s*(?:60|62|76)(?:[.,]\d{1,2})?|\b(?:60|62|76)(?:[.,]\d{1,2})?\b\s*сч(?:е|ё)т)/iu.test(normalized);
if (hasOpenItemsAccountCue) {
return unicodeBridgeResolution("open_items_by_counterparty_or_contract", "medium", "open_items_signal_detected");
}
const hasCounterpartyShipmentItemFlowCue = /(?:отгруж(?:ал|али|ен[аоы]?|енн(?:ый|ая|ое|ые)?))/iu.test(normalized) &&
/(?:товар|услуг|позици|номенклатур)/iu.test(normalized) &&
!/(?:выбранн(?:ый|ому)\s+объект|selected\s+object)/iu.test(normalized);
if (hasCounterpartyShipmentItemFlowCue) {
return unicodeBridgeResolution("list_documents_by_counterparty", "medium", "counterparty_item_flow_signal_detected");
}
const hasHighestValueCustomerCue = /(?:сам(?:ый|ые|ая|ое)|топ|кто|какой|какие|больше\s+всего|наибольш)/iu.test(normalized) &&
/(?:доходн|выручк|денег|деньг|оборот|поступлен|прин[её]с)/iu.test(normalized) &&
/(?:клиент|покупател|заказчик|контрагент)/iu.test(normalized);
if (!hasContractCue && hasHighestValueCustomerCue) {
return unicodeBridgeResolution("customer_revenue_and_payments", "high", "unicode_customer_revenue_bridge_signal_detected");
}
if (hasBidirectionalValueFlowComparisonSignal(normalized)) {
return unicodeBridgeResolution("unknown", "high", "unicode_bidirectional_value_flow_deferred_to_discovery");
}
@@ -1749,9 +1766,11 @@ function resolveUnicodeAddressIntentBridge(text) {
if (/(?:ндс|vat)/iu.test(normalized)) {
const hasVatDebtCue = /(?:долг|должн|подтвержд)/iu.test(normalized);
const hasTaxPeriodCue = /(?:налогов|налоговую|бюджет|декларац|квартал|\b[1-4]\s*кв)/iu.test(normalized);
if (hasTaxPeriodCue &&
const hasVatMonthPeriodCue = /(?:за|на|в)\s+(?:январ|феврал|март|апрел|ма[йя]|июн|июл|август|сентябр|октябр|ноябр|декабр)\S*(?:\s+(?:19|20)\d{2})?/iu.test(normalized) &&
!/\b\d{1,2}\s+(?:январ|феврал|март|апрел|ма[йя]|июн|июл|август|сентябр|октябр|ноябр|декабр)\S*(?:\s+(?:19|20)\d{2})?/iu.test(normalized);
if ((hasTaxPeriodCue || (hasVatMonthPeriodCue && !hasVatDebtCue)) &&
/(?:скольк|скока|надо|нужно|заплат|уплат|оплат|прикин)/iu.test(normalized)) {
return unicodeBridgeResolution("vat_liability_confirmed_for_tax_period", "high", "vat_tax_period_confirmed_signal_detected");
return unicodeBridgeResolution("vat_liability_confirmed_for_tax_period", "high", "vat_liability_confirmed_tax_period_signal_detected");
}
if (/(?:прогноз|прикин|план)/iu.test(normalized) ||
(!hasVatDebtCue && /(?:надо|нужно)\s+(?:заплат|оплат|уплат)/iu.test(normalized))) {
+23 -10
View File
@@ -586,7 +586,7 @@ const CONTRACTS_BY_COUNTERPARTY_QUERY_TEMPLATE = `
`;
const VAT_PAYABLE_FORECAST_QUERY_TEMPLATE = `
ВЫБРАТЬ
ДАТАВРЕМЯ(2000, 1, 1, 0, 0, 0) КАК Период,
__PERIOD_EXPR__ КАК Период,
"VAT_68_CREDIT" КАК Регистратор,
"68" КАК СчетДт,
"" КАК СчетКт,
@@ -600,7 +600,7 @@ const VAT_PAYABLE_FORECAST_QUERY_TEMPLATE = `
__WHERE_CLAUSE__
ОБЪЕДИНИТЬ ВСЕ
ВЫБРАТЬ
ДАТАВРЕМЯ(2000, 1, 1, 0, 0, 0) КАК Период,
__PERIOD_EXPR__ КАК Период,
"VAT_68_DEBIT" КАК Регистратор,
"68" КАК СчетДт,
"" КАК СчетКт,
@@ -614,7 +614,7 @@ __WHERE_CLAUSE__
__WHERE_CLAUSE__
ОБЪЕДИНИТЬ ВСЕ
ВЫБРАТЬ
ДАТАВРЕМЯ(2000, 1, 1, 0, 0, 0) КАК Период,
__PERIOD_EXPR__ КАК Период,
"VAT_19_DEBIT" КАК Регистратор,
"19" КАК СчетДт,
"" КАК СчетКт,
@@ -628,7 +628,7 @@ __WHERE_CLAUSE__
__WHERE_CLAUSE__
ОБЪЕДИНИТЬ ВСЕ
ВЫБРАТЬ
ДАТАВРЕМЯ(2000, 1, 1, 0, 0, 0) КАК Период,
__PERIOD_EXPR__ КАК Период,
"VAT_19_CREDIT" КАК Регистратор,
"19" КАК СчетДт,
"" КАК СчетКт,
@@ -1388,12 +1388,25 @@ function buildAddressRecipePlan(recipe, filters) {
.replaceAll("__WHERE_OUT_VALUE__", buildContractValueWhereClause(filters, "БанкСписание.Дата", "БанкСписание.ДоговорКонтрагента"))
.replaceAll("__ORDER_DIRECTION__", resolveOrderDirection(filters.sort))
: recipe.query_template === "vat_payable_forecast_profile"
? VAT_PAYABLE_FORECAST_QUERY_TEMPLATE
.replaceAll("__WHERE_CLAUSE__", buildManagementWhereClause(filters, "Движения.Период"))
.replaceAll("__VAT68_KT_MATCH__", buildAccountPrefixPredicate("Движения.СчетКт", config_1.VAT_PAYABLE_68_PREFIXES))
.replaceAll("__VAT68_DT_MATCH__", buildAccountPrefixPredicate("Движения.СчетДт", config_1.VAT_PAYABLE_68_PREFIXES))
.replaceAll("__VAT19_DT_MATCH__", buildAccountPrefixPredicate("Движения.СчетДт", config_1.VAT_PAYABLE_19_PREFIXES))
.replaceAll("__VAT19_KT_MATCH__", buildAccountPrefixPredicate("Движения.СчетКт", config_1.VAT_PAYABLE_19_PREFIXES))
? (() => {
const periodExpr = (typeof filters.period_to === "string" && filters.period_to.trim().length > 0
? toDateTimeExpr(filters.period_to, true)
: null) ??
(typeof filters.as_of_date === "string" && filters.as_of_date.trim().length > 0
? toDateTimeExpr(filters.as_of_date, true)
: null) ??
(typeof filters.period_from === "string" && filters.period_from.trim().length > 0
? toDateTimeExpr(filters.period_from, true)
: null) ??
"ДАТАВРЕМЯ(2000, 1, 1, 0, 0, 0)";
return VAT_PAYABLE_FORECAST_QUERY_TEMPLATE
.replaceAll("__WHERE_CLAUSE__", buildManagementWhereClause(filters, "Движения.Период"))
.replaceAll("__PERIOD_EXPR__", periodExpr)
.replaceAll("__VAT68_KT_MATCH__", buildAccountPrefixPredicate("Движения.СчетКт", config_1.VAT_PAYABLE_68_PREFIXES))
.replaceAll("__VAT68_DT_MATCH__", buildAccountPrefixPredicate("Движения.СчетДт", config_1.VAT_PAYABLE_68_PREFIXES))
.replaceAll("__VAT19_DT_MATCH__", buildAccountPrefixPredicate("Движения.СчетДт", config_1.VAT_PAYABLE_19_PREFIXES))
.replaceAll("__VAT19_KT_MATCH__", buildAccountPrefixPredicate("Движения.СчетКт", config_1.VAT_PAYABLE_19_PREFIXES));
})()
: recipe.query_template === "vat_liability_confirmed_tax_period_profile"
? (() => {
const periodToExpr = (typeof filters.period_to === "string" && filters.period_to.trim().length > 0
@@ -1150,6 +1150,8 @@ function deriveIntentWithFollowupContext(detectedIntent, userMessage, followupCo
return detectedIntent;
}
const previousFilters = followupContext.previous_filters ?? {};
const previousPeriodFrom = toNonEmptyString(previousFilters.period_from);
const previousPeriodTo = toNonEmptyString(previousFilters.period_to);
const previousContract = toNonEmptyString(previousFilters.contract);
const previousCounterparty = toNonEmptyString(previousFilters.counterparty);
const previousContractFromAnchor = followupContext.previous_anchor_type === "contract" ? toNonEmptyString(followupContext.previous_anchor_value) : null;
@@ -1158,6 +1160,9 @@ function deriveIntentWithFollowupContext(detectedIntent, userMessage, followupCo
const hasPreviousCounterparty = Boolean(previousCounterparty ?? previousCounterpartyFromAnchor);
const hasAnyPartyAnchor = hasPreviousContract || hasPreviousCounterparty;
const isVatFollowup = hasVatCue(normalizedMessage);
const samePeriodVatFollowup = isVatFollowup &&
hasSamePeriodHint(normalizedMessage) &&
Boolean(previousPeriodFrom || previousPeriodTo);
const previousIsInventoryFamily = isInventoryIntent(sourceIntent ?? undefined);
const rootIsInventoryFamily = isInventoryIntent(followupContext.root_intent ?? undefined);
const inventoryLineageActive = previousIsInventoryFamily ||
@@ -1173,13 +1178,24 @@ function deriveIntentWithFollowupContext(detectedIntent, userMessage, followupCo
if (inventoryPurchaseDateVatBridge &&
(detectedIntent.intent === "unknown" ||
detectedIntent.intent === sourceIntent ||
detectedIntent.intent === "vat_payable_confirmed_as_of_date")) {
detectedIntent.intent === "vat_payable_confirmed_as_of_date" ||
detectedIntent.intent === "vat_payable_forecast")) {
return {
intent: "vat_liability_confirmed_for_tax_period",
confidence: "low",
reasons: [...detectedIntent.reasons, "intent_adjusted_to_inventory_purchase_date_vat_bridge"]
};
}
if (samePeriodVatFollowup &&
(detectedIntent.intent === "vat_payable_confirmed_as_of_date" ||
detectedIntent.intent === "vat_payable_forecast" ||
detectedIntent.intent === "unknown")) {
return {
intent: "vat_liability_confirmed_for_tax_period",
confidence: "low",
reasons: [...detectedIntent.reasons, "intent_adjusted_to_vat_same_period_followup"]
};
}
if (detectedIntent.intent === "unknown" && isVatFollowup) {
const vatIntent = hasVatTaxPaymentCue(normalizedMessage)
? "vat_liability_confirmed_for_tax_period"
@@ -987,7 +987,12 @@ function extractShipmentCounterpartyValue(text: string): string | undefined {
}
function extractInstrumentalCounterpartyValue(text: string): string | undefined {
const match = String(text ?? "").match(
const source = String(text ?? "");
const match =
source.match(
/(?:\u043a\u043e\u043d\u0442\u0440\u0430\u0433\u0435\u043d\u0442\u043e\u043c|\u043f\u043e\u0441\u0442\u0430\u0432\u0449\u0438\u043a\u043e\u043c|\u043a\u043b\u0438\u0435\u043d\u0442\u043e\u043c|\u0437\u0430\u043a\u0430\u0437\u0447\u0438\u043a\u043e\u043c)\s+([\p{L}][\p{L}\p{N}._-]{1,})(?=[\s,.;:!?)]|$)/iu
) ??
source.match(
/(?:контрагентом|поставщиком|клиентом|заказчиком)\s+([\p{L}][\p{L}\p{N}._-]{1,})(?=[\s,.;:!?)]|$)/iu
);
if (!match) {
@@ -2031,6 +2031,43 @@ function resolveUnicodeAddressIntentBridge(text: string): AddressIntentResolutio
normalized
);
const hasOpenItemsAccountCue =
/(?:хвост|долг|незакрыт|вис)/iu.test(normalized) &&
/(?:сч(?:е|ё)т(?:а|у|ом|е|ов)?\s*(?:|#)?\s*(?:60|62|76)(?:[.,]\d{1,2})?|\b(?:60|62|76)(?:[.,]\d{1,2})?\b\s*сч(?:е|ё)т)/iu.test(
normalized
);
if (hasOpenItemsAccountCue) {
return unicodeBridgeResolution(
"open_items_by_counterparty_or_contract",
"medium",
"open_items_signal_detected"
);
}
const hasCounterpartyShipmentItemFlowCue =
/(?:отгруж(?:ал|али|ен[аоы]?|енн(?:ый|ая|ое|ые)?))/iu.test(normalized) &&
/(?:товар|услуг|позици|номенклатур)/iu.test(normalized) &&
!/(?:выбранн(?:ый|ому)\s+объект|selected\s+object)/iu.test(normalized);
if (hasCounterpartyShipmentItemFlowCue) {
return unicodeBridgeResolution(
"list_documents_by_counterparty",
"medium",
"counterparty_item_flow_signal_detected"
);
}
const hasHighestValueCustomerCue =
/(?:сам(?:ый|ые|ая|ое)|топ|кто|какой|какие|больше\s+всего|наибольш)/iu.test(normalized) &&
/(?:доходн|выручк|денег|деньг|оборот|поступлен|прин[её]с)/iu.test(normalized) &&
/(?:клиент|покупател|заказчик|контрагент)/iu.test(normalized);
if (!hasContractCue && hasHighestValueCustomerCue) {
return unicodeBridgeResolution(
"customer_revenue_and_payments",
"high",
"unicode_customer_revenue_bridge_signal_detected"
);
}
if (hasBidirectionalValueFlowComparisonSignal(normalized)) {
return unicodeBridgeResolution(
"unknown",
@@ -2419,14 +2456,21 @@ function resolveUnicodeAddressIntentBridge(text: string): AddressIntentResolutio
if (/(?:ндс|vat)/iu.test(normalized)) {
const hasVatDebtCue = /(?:долг|должн|подтвержд)/iu.test(normalized);
const hasTaxPeriodCue = /(?:налогов|налоговую|бюджет|декларац|квартал|\b[1-4]\s*кв)/iu.test(normalized);
const hasVatMonthPeriodCue =
/(?:за|на|в)\s+(?:январ|феврал|март|апрел|ма[йя]|июн|июл|август|сентябр|октябр|ноябр|декабр)\S*(?:\s+(?:19|20)\d{2})?/iu.test(
normalized
) &&
!/\b\d{1,2}\s+(?:январ|феврал|март|апрел|ма[йя]|июн|июл|август|сентябр|октябр|ноябр|декабр)\S*(?:\s+(?:19|20)\d{2})?/iu.test(
normalized
);
if (
hasTaxPeriodCue &&
(hasTaxPeriodCue || (hasVatMonthPeriodCue && !hasVatDebtCue)) &&
/(?:скольк|скока|надо|нужно|заплат|уплат|оплат|прикин)/iu.test(normalized)
) {
return unicodeBridgeResolution(
"vat_liability_confirmed_for_tax_period",
"high",
"vat_tax_period_confirmed_signal_detected"
"vat_liability_confirmed_tax_period_signal_detected"
);
}
if (
@@ -608,7 +608,7 @@ const CONTRACTS_BY_COUNTERPARTY_QUERY_TEMPLATE = `
const VAT_PAYABLE_FORECAST_QUERY_TEMPLATE = `
ВЫБРАТЬ
ДАТАВРЕМЯ(2000, 1, 1, 0, 0, 0) КАК Период,
__PERIOD_EXPR__ КАК Период,
"VAT_68_CREDIT" КАК Регистратор,
"68" КАК СчетДт,
"" КАК СчетКт,
@@ -622,7 +622,7 @@ const VAT_PAYABLE_FORECAST_QUERY_TEMPLATE = `
__WHERE_CLAUSE__
ОБЪЕДИНИТЬ ВСЕ
ВЫБРАТЬ
ДАТАВРЕМЯ(2000, 1, 1, 0, 0, 0) КАК Период,
__PERIOD_EXPR__ КАК Период,
"VAT_68_DEBIT" КАК Регистратор,
"68" КАК СчетДт,
"" КАК СчетКт,
@@ -636,7 +636,7 @@ __WHERE_CLAUSE__
__WHERE_CLAUSE__
ОБЪЕДИНИТЬ ВСЕ
ВЫБРАТЬ
ДАТАВРЕМЯ(2000, 1, 1, 0, 0, 0) КАК Период,
__PERIOD_EXPR__ КАК Период,
"VAT_19_DEBIT" КАК Регистратор,
"19" КАК СчетДт,
"" КАК СчетКт,
@@ -650,7 +650,7 @@ __WHERE_CLAUSE__
__WHERE_CLAUSE__
ОБЪЕДИНИТЬ ВСЕ
ВЫБРАТЬ
ДАТАВРЕМЯ(2000, 1, 1, 0, 0, 0) КАК Период,
__PERIOD_EXPR__ КАК Период,
"VAT_19_CREDIT" КАК Регистратор,
"19" КАК СчетДт,
"" КАК СчетКт,
@@ -1546,12 +1546,26 @@ export function buildAddressRecipePlan(
)
.replaceAll("__ORDER_DIRECTION__", resolveOrderDirection(filters.sort))
: recipe.query_template === "vat_payable_forecast_profile"
? VAT_PAYABLE_FORECAST_QUERY_TEMPLATE
.replaceAll("__WHERE_CLAUSE__", buildManagementWhereClause(filters, "Движения.Период"))
.replaceAll("__VAT68_KT_MATCH__", buildAccountPrefixPredicate("Движения.СчетКт", VAT_PAYABLE_68_PREFIXES))
.replaceAll("__VAT68_DT_MATCH__", buildAccountPrefixPredicate("Движения.СчетДт", VAT_PAYABLE_68_PREFIXES))
.replaceAll("__VAT19_DT_MATCH__", buildAccountPrefixPredicate("Движения.СчетДт", VAT_PAYABLE_19_PREFIXES))
.replaceAll("__VAT19_KT_MATCH__", buildAccountPrefixPredicate("Движения.СчетКт", VAT_PAYABLE_19_PREFIXES))
? (() => {
const periodExpr =
(typeof filters.period_to === "string" && filters.period_to.trim().length > 0
? toDateTimeExpr(filters.period_to, true)
: null) ??
(typeof filters.as_of_date === "string" && filters.as_of_date.trim().length > 0
? toDateTimeExpr(filters.as_of_date, true)
: null) ??
(typeof filters.period_from === "string" && filters.period_from.trim().length > 0
? toDateTimeExpr(filters.period_from, true)
: null) ??
"ДАТАВРЕМЯ(2000, 1, 1, 0, 0, 0)";
return VAT_PAYABLE_FORECAST_QUERY_TEMPLATE
.replaceAll("__WHERE_CLAUSE__", buildManagementWhereClause(filters, "Движения.Период"))
.replaceAll("__PERIOD_EXPR__", periodExpr)
.replaceAll("__VAT68_KT_MATCH__", buildAccountPrefixPredicate("Движения.СчетКт", VAT_PAYABLE_68_PREFIXES))
.replaceAll("__VAT68_DT_MATCH__", buildAccountPrefixPredicate("Движения.СчетДт", VAT_PAYABLE_68_PREFIXES))
.replaceAll("__VAT19_DT_MATCH__", buildAccountPrefixPredicate("Движения.СчетДт", VAT_PAYABLE_19_PREFIXES))
.replaceAll("__VAT19_KT_MATCH__", buildAccountPrefixPredicate("Движения.СчетКт", VAT_PAYABLE_19_PREFIXES));
})()
: recipe.query_template === "vat_liability_confirmed_tax_period_profile"
? (() => {
const periodToExpr =
@@ -1439,6 +1439,8 @@ function deriveIntentWithFollowupContext(
return detectedIntent;
}
const previousFilters = followupContext.previous_filters ?? {};
const previousPeriodFrom = toNonEmptyString(previousFilters.period_from);
const previousPeriodTo = toNonEmptyString(previousFilters.period_to);
const previousContract = toNonEmptyString(previousFilters.contract);
const previousCounterparty = toNonEmptyString(previousFilters.counterparty);
const previousContractFromAnchor =
@@ -1449,6 +1451,10 @@ function deriveIntentWithFollowupContext(
const hasPreviousCounterparty = Boolean(previousCounterparty ?? previousCounterpartyFromAnchor);
const hasAnyPartyAnchor = hasPreviousContract || hasPreviousCounterparty;
const isVatFollowup = hasVatCue(normalizedMessage);
const samePeriodVatFollowup =
isVatFollowup &&
hasSamePeriodHint(normalizedMessage) &&
Boolean(previousPeriodFrom || previousPeriodTo);
const previousIsInventoryFamily = isInventoryIntent(sourceIntent ?? undefined);
const rootIsInventoryFamily = isInventoryIntent(followupContext.root_intent ?? undefined);
const inventoryLineageActive =
@@ -1472,7 +1478,8 @@ function deriveIntentWithFollowupContext(
inventoryPurchaseDateVatBridge &&
(detectedIntent.intent === "unknown" ||
detectedIntent.intent === sourceIntent ||
detectedIntent.intent === "vat_payable_confirmed_as_of_date")
detectedIntent.intent === "vat_payable_confirmed_as_of_date" ||
detectedIntent.intent === "vat_payable_forecast")
) {
return {
intent: "vat_liability_confirmed_for_tax_period",
@@ -1481,6 +1488,19 @@ function deriveIntentWithFollowupContext(
};
}
if (
samePeriodVatFollowup &&
(detectedIntent.intent === "vat_payable_confirmed_as_of_date" ||
detectedIntent.intent === "vat_payable_forecast" ||
detectedIntent.intent === "unknown")
) {
return {
intent: "vat_liability_confirmed_for_tax_period",
confidence: "low",
reasons: [...detectedIntent.reasons, "intent_adjusted_to_vat_same_period_followup"]
};
}
if (detectedIntent.intent === "unknown" && isVatFollowup) {
const vatIntent: AddressIntent = hasVatTaxPaymentCue(normalizedMessage)
? "vat_liability_confirmed_for_tax_period"
@@ -30,6 +30,61 @@ describe("counterparty shipment item flow and open-items routing", () => {
expect(result.reasons).toContain("counterparty_item_flow_signal_detected");
});
it("keeps plain Russian counterparty item-flow wording out of stale inventory context", async () => {
executeAddressMcpQueryMock
.mockResolvedValueOnce({
fetched_rows: 1,
matched_rows: 1,
raw_rows: [
{
Counterparty: "Чепурнов П.Д.",
Registrator: "Чепурнов П.Д."
}
],
rows: [],
error: null
})
.mockResolvedValueOnce({
fetched_rows: 1,
matched_rows: 1,
raw_rows: [
{
Period: "2022-01-20T12:00:03Z",
Registrator: "Поступление товаров и услуг 000000001 от 20.01.2022",
AccountDt: "41.01",
AccountKt: "60.01",
Amount: 890660,
Nomenclature: "Услуги по договору",
Counterparty: "Чепурнов П.Д.",
Contract: "Договор № 11/1 от 25.11.2020 г.",
Organization: 'ООО "Альтернатива Плюс"'
}
],
rows: [],
error: null
});
const service = new AddressQueryService();
const result = await service.tryHandle("что нам отгружал чепурнов? какой товар или услугу?", {
followupContext: {
previous_intent: "inventory_on_hand_as_of_date",
target_intent: "inventory_on_hand_as_of_date",
previous_filters: {
organization: 'ООО "Альтернатива Плюс"',
as_of_date: "2026-04-24"
},
previous_anchor_type: "organization",
previous_anchor_value: 'ООО "Альтернатива Плюс"'
}
});
expect(result?.handled).toBe(true);
expect(result?.debug.detected_intent).toBe("list_documents_by_counterparty");
expect(result?.debug.selected_recipe).toBe("address_documents_by_counterparty_v1");
expect(String(result?.reply_text ?? "")).toContain("Чепурнов П.Д.");
expect(String(result?.reply_text ?? "")).toContain("Услуги по договору");
});
it("routes account 60 tails wording to open items intent", () => {
const result = resolveAddressIntent("хвосты покажи по счету 60 на август 2022");
expect(result.intent).toBe("open_items_by_counterparty_or_contract");
@@ -60,8 +115,10 @@ describe("counterparty shipment item flow and open-items routing", () => {
});
it("uses purchase document query for fuzzy counterparty item-flow wording", async () => {
executeAddressMcpQueryMock
.mockResolvedValueOnce({
executeAddressMcpQueryMock.mockImplementation(async (request?: { query?: string }) => {
const query = String(request?.query ?? "");
if (query.includes("Справочник.Контрагенты")) {
return {
fetched_rows: 1,
matched_rows: 1,
raw_rows: [
@@ -72,8 +129,9 @@ describe("counterparty shipment item flow and open-items routing", () => {
],
rows: [],
error: null
})
.mockResolvedValueOnce({
};
}
return {
fetched_rows: 2,
matched_rows: 2,
raw_rows: [
@@ -104,7 +162,8 @@ describe("counterparty shipment item flow and open-items routing", () => {
],
rows: [],
error: null
});
};
});
const service = new AddressQueryService();
const result = await service.tryHandle(
@@ -117,4 +117,40 @@ describe("counterparty lifecycle organization scope regressions", () => {
expect(result?.debug.extracted_filters?.counterparty).toBeUndefined();
expect(String(result?.reply_text ?? "")).toContain('ООО "Ромашка"');
});
it("routes who-is-the-highest-value-customer wording to revenue ranking, not lifecycle activity", async () => {
executeAddressMcpQueryMock.mockResolvedValueOnce({
fetched_rows: 2,
matched_rows: 2,
raw_rows: [
{
Period: "2021-01-15T00:00:00Z",
Registrator: "CP_CUSTOMER_ACTIVITY",
AccountDt: "51",
AccountKt: "62.01",
Amount: 150000,
Counterparty: "Группа СВК"
},
{
Period: "2021-02-20T00:00:00Z",
Registrator: "CP_CUSTOMER_ACTIVITY",
AccountDt: "51",
AccountKt: "62.01",
Amount: 80000,
Counterparty: "Чепурнов П.Д."
}
],
rows: [],
error: null
});
const service = new AddressQueryService();
const result = await service.tryHandle("кто у нас самый доходный клиент за все время");
expect(result?.handled).toBe(true);
expect(result?.debug.detected_intent).toBe("customer_revenue_and_payments");
expect(result?.debug.selected_recipe).toBe("address_customer_revenue_and_payments_v1");
expect(String(result?.reply_text ?? "")).toContain("Самый доходный клиент");
expect(String(result?.reply_text ?? "")).toContain("Группа СВК");
});
});
@@ -3955,11 +3955,11 @@ describe("address query limited taxonomy and stage diagnostics", { timeout: 1500
expect(result?.debug.mcp_call_status).not.toBe("skipped");
});
it("returns factual confirmed VAT snapshot instead of partial when payable rows are absent", async () => {
it("returns factual confirmed VAT tax-period answer instead of partial when payable rows are absent", async () => {
const service = new AddressQueryService();
const result = await service.tryHandle("скок ндс платить надо на март 2020");
expect(result?.handled).toBe(true);
expect(result?.debug.detected_intent).toBe("vat_payable_confirmed_as_of_date");
expect(result?.debug.detected_intent).toBe("vat_liability_confirmed_for_tax_period");
expect(["FACTUAL_LIST", "FACTUAL_SUMMARY"]).toContain(result?.response_type);
expect(result?.reply_type).not.toBe("partial_coverage");
expect(result?.debug.result_mode).toBe("confirmed_balance");
@@ -4692,13 +4692,13 @@ describe("address decompose stage follow-up carryover", () => {
period_to: "2020-12-31"
},
previous_anchor_type: "counterparty",
previous_anchor_value: "?П Калинин Н.М.",
previous_anchor_value: "ИП Калинин Н.М.",
resolved_counterparty_from_display: true
});
expect(result).not.toBeNull();
expect(result?.mode.mode).toBe("address_query");
expect(result?.intent.intent).toBe("customer_revenue_and_payments");
expect(result?.filters.extracted_filters.counterparty).toBe("?П Калинин Н.М.");
expect(result?.filters.extracted_filters.counterparty).toBe("ИП Калинин Н.М.");
expect(result?.filters.extracted_filters.period_from).toBe("2020-01-01");
expect(result?.filters.extracted_filters.period_to).toBe("2020-12-31");
expect(
@@ -5201,6 +5201,19 @@ describe("address recipe catalog counterparty filtering", () => {
expect(plan.query).not.toContain("ПРЕДСТАВЛЕНИЕ(Движения.СчетДт) ПОДОБНО");
});
it("materializes VAT forecast aggregate inside the requested period window", () => {
const filters = extractAddressFilters(
"прикинь какой ндс нам надо заплатить на февраль 2017",
"vat_payable_forecast"
).extracted_filters;
const selected = selectAddressRecipe("vat_payable_forecast", filters);
expect(selected.selected_recipe).toBeTruthy();
const plan = buildAddressRecipePlan(selected.selected_recipe!, filters);
expect(plan.query).toContain("ДАТАВРЕМЯ(2017, 2, 28, 23, 59, 59) КАК Период");
expect(plan.query).not.toContain("ДАТАВРЕМЯ(2000, 1, 1, 0, 0, 0) КАК Период");
});
it("builds confirmed VAT tax-period query from sales and purchase VAT books", () => {
const filters = extractAddressFilters(
"сколько ндс надо заплатить в налоговую за декабрь 2019",