Open-World: включить маржинальность выбранной номенклатуры

This commit is contained in:
2026-05-04 08:40:27 +03:00
parent edab736a6d
commit 7294eca381
23 changed files with 472 additions and 56 deletions
@@ -85,7 +85,7 @@ describe("address capability policy", () => {
expect(chainDecision.capability_route_mode).toBe("exact");
});
it("enables purchase-to-sale trace and aging by purchase date", () => {
it("enables purchase-to-sale trace, item profitability, and aging by purchase date", () => {
const chainDecision = resolveAddressCapabilityRouteDecision("inventory_purchase_to_sale_chain");
expect(chainDecision.capability_id).toBe("inventory_inventory_purchase_to_sale_chain");
expect(chainDecision.capability_route_mode).toBe("exact");
@@ -93,6 +93,14 @@ describe("address capability policy", () => {
expect(chainDecision.capability_route_reason).toBe("inventory_purchase_to_sale_chain_route_enabled");
expect(isCapabilityRouteBlocked(chainDecision)).toBe(false);
const profitabilityDecision = resolveAddressCapabilityRouteDecision("inventory_profitability_for_item");
expect(profitabilityDecision.capability_id).toBe("inventory_inventory_profitability_for_item");
expect(profitabilityDecision.capability_layer).toBe("compute");
expect(profitabilityDecision.capability_route_mode).toBe("exact");
expect(profitabilityDecision.capability_route_enabled).toBe(true);
expect(profitabilityDecision.capability_route_reason).toBe("inventory_profitability_route_enabled");
expect(isCapabilityRouteBlocked(profitabilityDecision)).toBe(false);
const agingDecision = resolveAddressCapabilityRouteDecision("inventory_aging_by_purchase_date");
expect(agingDecision.capability_id).toBe("inventory_inventory_aging_by_purchase_date");
expect(agingDecision.capability_route_mode).toBe("exact");
@@ -49,19 +49,59 @@ describe("inventory profitability selected-object regressions", () => {
expect(result?.filters.extracted_filters.period_to).toBe("2020-05-31");
});
it("returns a truthful recipe visibility gap until item profitability gets a dedicated recipe", async () => {
it("answers selected-object profitability with a bounded document spread instead of a recipe gap", async () => {
executeAddressMcpQueryMock.mockResolvedValueOnce({
fetched_rows: 2,
matched_rows: 2,
raw_rows: [
{
Period: "2020-05-10T00:00:00Z",
Registrator: "Поступление товаров и услуг 000000001 от 10.05.2020",
AccountDt: "41.01",
AccountKt: "60.01",
Amount: 500,
Quantity: 10,
SubcontoDt1: "Четки Пост (84*117)",
SubcontoDt3: "Основной склад",
Counterparty: "ООО \\Поставщик\\",
Organization: "ООО \\Альтернатива Плюс\\"
},
{
Period: "2020-05-20T00:00:00Z",
Registrator: "Реализация товаров и услуг 000000017 от 20.05.2020",
AccountDt: "62.01",
AccountKt: "41.01",
Amount: 900,
Quantity: 10,
SubcontoKt1: "Четки Пост (84*117)",
SubcontoKt3: "Основной склад",
Counterparty: "ИП Покупатель",
Organization: "ООО \\Альтернатива Плюс\\"
}
],
rows: [],
error: null
});
const service = new AddressQueryService();
const result = await service.tryHandle(selectedObjectProfitabilityMessage, {
followupContext
});
expect(result?.handled).toBe(true);
expect(result?.response_type).toBe("LIMITED_WITH_REASON");
expect(result?.response_type).toBe("FACTUAL_SUMMARY");
expect(result?.debug.detected_intent).toBe("inventory_profitability_for_item");
expect(result?.debug.selected_recipe).toBeNull();
expect(result?.debug.limited_reason_category).toBe("recipe_visibility_gap");
expect(result?.debug.selected_recipe).toBe("address_inventory_profitability_for_item_v1");
expect(result?.debug.capability_id).toBe("inventory_inventory_profitability_for_item");
expect(result?.debug.mcp_call_status).toBe("matched_non_empty");
expect(result?.debug.extracted_filters?.item).toBe("Четки Пост (84*117)");
expect(String(result?.reply_text ?? "")).toContain("Четки Пост (84*117)");
expect(executeAddressMcpQueryMock).not.toHaveBeenCalled();
const reply = String(result?.reply_text ?? "");
expect(reply).toContain("Четки Пост (84*117)");
expect(reply).toContain("900");
expect(reply).toContain("500");
expect(reply).toContain("400");
expect(reply).toContain("Маржинальность");
expect(reply).toContain("не чистая прибыль компании");
expect(executeAddressMcpQueryMock).toHaveBeenCalledTimes(1);
});
});
@@ -355,6 +355,20 @@ describe("address query shape classifier", () => {
expect(plan.query).toContain("ПРЕДСТАВЛЕНИЕ(Товары.Ссылка.Контрагент) КАК Контрагент");
});
it("builds item profitability query from purchase and sale document rows", () => {
const selected = selectAddressRecipe("inventory_profitability_for_item", {
item: "Шкаф картотечный"
});
expect(selected.selected_recipe?.recipe_id).toBe("address_inventory_profitability_for_item_v1");
const plan = buildAddressRecipePlan(selected.selected_recipe!, {
item: "Шкаф картотечный"
});
expect(plan.query).toContain("Документ.ПоступлениеТоваровУслуг.Товары КАК Товары");
expect(plan.query).toContain("ОБЪЕДИНИТЬ ВСЕ");
expect(plan.query).toContain("Документ.РеализацияТоваровУслуг.Товары КАК Товары");
expect(plan.query).toContain("ПРЕДСТАВЛЕНИЕ(Товары.Ссылка.Контрагент) КАК Контрагент");
});
it("renders inventory purchase documents from purchase-side 41.01 movements", () => {
const reply = composeFactualReply(
"inventory_purchase_documents_for_item",
@@ -71,6 +71,18 @@ describe("assistant runtime contract registry", () => {
expect(contract?.required_scenario_families).toContain("pronoun_followup");
});
it("declares selected-item profitability as a bounded focus-object contract", () => {
const contract = getAssistantCapabilityContractByIntent("inventory_profitability_for_item");
expect(contract?.capability_id).toBe("inventory_inventory_profitability_for_item");
expect(contract?.requires_focus_object).toBe(true);
expect(contract?.accepted_focus_object_kinds).toEqual(["inventory_item", "item"]);
expect(contract?.supported_transition_classes).toEqual(["T3", "T4", "T5", "T7"]);
expect(contract?.required_anchors).toEqual(["item"]);
expect(contract?.result_shape).toBe("item_revenue_purchase_cost_spread_margin_proxy");
expect(contract?.answer_object_shape).toBe("inventory_profitability_bundle");
expect(contract?.bundle_reuse_policy).toBe("sale_trace_bundle_preferred");
});
it("declares root financial exact capabilities for debt and vat snapshots", () => {
const receivables = getAssistantCapabilityContract("confirmed_receivables_as_of_date");
const payables = getAssistantCapabilityContract("confirmed_payables_as_of_date");