diff --git a/llm_normalizer/backend/dist/services/addressQueryService.js b/llm_normalizer/backend/dist/services/addressQueryService.js index c0848ac..db1b341 100644 --- a/llm_normalizer/backend/dist/services/addressQueryService.js +++ b/llm_normalizer/backend/dist/services/addressQueryService.js @@ -211,6 +211,22 @@ function deriveTaxQuarterWindowForDate(value) { period_to: `${year}-${String(quarterEndMonth).padStart(2, "0")}-${String(quarterEndDay).padStart(2, "0")}` }; } +function isFullCalendarMonthWindow(periodFrom, periodTo) { + const fromIso = normalizeIsoDateForQuery(periodFrom); + const toIso = normalizeIsoDateForQuery(periodTo); + if (!fromIso || !toIso) { + return false; + } + const fromMatch = fromIso.match(/^(\d{4})-(\d{2})-(\d{2})$/); + const toMatch = toIso.match(/^(\d{4})-(\d{2})-(\d{2})$/); + if (!fromMatch || !toMatch || fromMatch[1] !== toMatch[1] || fromMatch[2] !== toMatch[2] || fromMatch[3] !== "01") { + return false; + } + const year = Number(fromMatch[1]); + const month = Number(fromMatch[2]); + const lastDay = new Date(Date.UTC(year, month, 0)).getUTCDate(); + return toMatch[3] === String(lastDay).padStart(2, "0"); +} function deriveMonthWindowForDate(value) { const isoDate = normalizeIsoDateForQuery(value); if (!isoDate) { @@ -3003,8 +3019,12 @@ class AddressQueryService { }) }); } + const vatTaxPeriodFromCarriedMonth = intent.intent === "vat_liability_confirmed_for_tax_period" && + (filters.warnings.includes("period_from_from_followup_context") || + filters.warnings.includes("period_to_from_followup_context")) && + isFullCalendarMonthWindow(filters.extracted_filters.period_from, filters.extracted_filters.period_to); if (intent.intent === "vat_liability_confirmed_for_tax_period" && - filters.warnings.includes("period_derived_from_month_phrase")) { + (filters.warnings.includes("period_derived_from_month_phrase") || vatTaxPeriodFromCarriedMonth)) { const taxQuarterWindow = deriveTaxQuarterWindowForDate(filters.extracted_filters.period_to); if (taxQuarterWindow) { filters.extracted_filters = { diff --git a/llm_normalizer/backend/src/services/addressQueryService.ts b/llm_normalizer/backend/src/services/addressQueryService.ts index b49e6a4..43099eb 100644 --- a/llm_normalizer/backend/src/services/addressQueryService.ts +++ b/llm_normalizer/backend/src/services/addressQueryService.ts @@ -355,6 +355,23 @@ function deriveTaxQuarterWindowForDate(value: unknown): { period_from: string; p }; } +function isFullCalendarMonthWindow(periodFrom: unknown, periodTo: unknown): boolean { + const fromIso = normalizeIsoDateForQuery(periodFrom); + const toIso = normalizeIsoDateForQuery(periodTo); + if (!fromIso || !toIso) { + return false; + } + const fromMatch = fromIso.match(/^(\d{4})-(\d{2})-(\d{2})$/); + const toMatch = toIso.match(/^(\d{4})-(\d{2})-(\d{2})$/); + if (!fromMatch || !toMatch || fromMatch[1] !== toMatch[1] || fromMatch[2] !== toMatch[2] || fromMatch[3] !== "01") { + return false; + } + const year = Number(fromMatch[1]); + const month = Number(fromMatch[2]); + const lastDay = new Date(Date.UTC(year, month, 0)).getUTCDate(); + return toMatch[3] === String(lastDay).padStart(2, "0"); +} + function deriveMonthWindowForDate(value: unknown): { period_from: string; period_to: string } | null { const isoDate = normalizeIsoDateForQuery(value); if (!isoDate) { @@ -3716,9 +3733,14 @@ export class AddressQueryService { }) }); } + const vatTaxPeriodFromCarriedMonth = + intent.intent === "vat_liability_confirmed_for_tax_period" && + (filters.warnings.includes("period_from_from_followup_context") || + filters.warnings.includes("period_to_from_followup_context")) && + isFullCalendarMonthWindow(filters.extracted_filters.period_from, filters.extracted_filters.period_to); if ( intent.intent === "vat_liability_confirmed_for_tax_period" && - filters.warnings.includes("period_derived_from_month_phrase") + (filters.warnings.includes("period_derived_from_month_phrase") || vatTaxPeriodFromCarriedMonth) ) { const taxQuarterWindow = deriveTaxQuarterWindowForDate(filters.extracted_filters.period_to); if (taxQuarterWindow) { diff --git a/llm_normalizer/backend/tests/addressQueryRuntimeM23.test.ts b/llm_normalizer/backend/tests/addressQueryRuntimeM23.test.ts index a417c41..e5f0de0 100644 --- a/llm_normalizer/backend/tests/addressQueryRuntimeM23.test.ts +++ b/llm_normalizer/backend/tests/addressQueryRuntimeM23.test.ts @@ -4102,6 +4102,33 @@ describe("address query limited taxonomy and stage diagnostics", { timeout: 9000 expect(result?.debug.route_expectation_status).toBe("matched"); }); + it("normalizes carried monthly debt period into VAT tax quarter for same-period follow-up", async () => { + const service = new AddressQueryService(); + const result = await service.tryHandle("а какой ндс мы должны примерно заплатить за этот период?", { + followupContext: { + previous_intent: "receivables_confirmed_as_of_date", + previous_filters: { + organization: 'ООО "Альтернатива Плюс"', + period_from: "2017-05-01", + period_to: "2017-05-31", + as_of_date: "2017-05-31" + }, + previous_anchor_type: "organization", + previous_anchor_value: 'ООО "Альтернатива Плюс"', + target_intent: "vat_liability_confirmed_for_tax_period" + } + }); + + expect(result?.handled).toBe(true); + expect(result?.debug.detected_intent).toBe("vat_liability_confirmed_for_tax_period"); + expect(result?.debug.selected_recipe).toBe("address_vat_liability_confirmed_tax_period_v1"); + expect(result?.debug.extracted_filters.period_from).toBe("2017-04-01"); + expect(result?.debug.extracted_filters.period_to).toBe("2017-06-30"); + expect(result?.debug.reasons).toContain("period_from_from_followup_context"); + expect(result?.debug.reasons).toContain("period_derived_from_tax_quarter_for_confirmed_vat_liability"); + expect(String(result?.reply_text ?? "")).toContain("01.04.2017..30.06.2017"); + }, 35000); + it("routes customer lifecycle question into dedicated aggregate recipe", async () => { const service = new AddressQueryService(); const result = await service.tryHandle("Какие заказчики работали с нами в 2020 году?");