diff --git a/src/views/dashboards/superUserDashboard/vehicle/displays/XLVaskUsageLog.vue b/src/views/dashboards/superUserDashboard/vehicle/displays/XLVaskUsageLog.vue
deleted file mode 100644
index 1785882b..00000000
--- a/src/views/dashboards/superUserDashboard/vehicle/displays/XLVaskUsageLog.vue
+++ /dev/null
@@ -1,849 +0,0 @@
-
-
-
- {{ t('tables.xlvask.usage_log_title') }}
-
- {{ t('tables.xlvask.create_order_all') }}
- {{ t('tables.xlvask.refresh') }}
-
-
-
-
-
-
- {{ t('tables.common.customer') }}
- {{ t('tables.common.registration_number') }}
- {{ t('tables.common.price') }}
- {{ t('tables.common.start_time') }}
- {{ t('tables.common.end_time') }}
- {{ t('tables.common.location') }}
-
-
-
-
-
-
-
- {{ t('tables.xlvask.usage_log_empty') }}
-
-
-
-
-
- { /* status indicator clicked */ }"
- />
-
- {{ usage.CustomerId }}
- {{ usage.RegistrationNumber }}
-
-
-
- {{ formatDate(usage.StartTime) }}
- {{ formatDate(usage.FinishTime) }}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {{ t('tables.common.wash_item') }} ({{ usage.WashItems.length }})
-
-
-
-
- {{ t('tables.common.wash_item') }}
- {{ t('tables.common.count') }}
- {{ t('tables.common.price') }}
-
-
-
-
- {{ getProductName(item) }}
- {{ item.Count }}
- {{ SessionUser.functions.currency.toLocal(Number(item.PriceIncVat ?? 0).toFixed(2)) }}
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/src/views/dashboards/superUserDashboard/xlvask/pages/XLVaskCustomersPage.vue b/src/views/dashboards/superUserDashboard/xlvask/pages/XLVaskCustomersPage.vue
index 0ef4c22e..a1974ce1 100644
--- a/src/views/dashboards/superUserDashboard/xlvask/pages/XLVaskCustomersPage.vue
+++ b/src/views/dashboards/superUserDashboard/xlvask/pages/XLVaskCustomersPage.vue
@@ -35,14 +35,14 @@ const fetchObjects = () => {
console.error("XL-Vask: customer list returned non-OK status.", { status: response?.status });
Swal.fire({
icon: 'error',
- title: t('invoicing_period.xlvask_autopilot.errors.load_customers_failed'),
+ title: t('invoicing_period.xlvask_review.errors.load_customers_failed'),
});
}
}).catch((error) => {
console.error("XL-Vask: failed to load customers.", { error });
Swal.fire({
icon: 'error',
- title: t('invoicing_period.xlvask_autopilot.errors.load_customers_failed'),
+ title: t('invoicing_period.xlvask_review.errors.load_customers_failed'),
});
}).finally(() => {
isLoading.value = false;
diff --git a/src/views/dashboards/superUserDashboard/xlvask/pages/XLVaskUsageLogsPage.vue b/src/views/dashboards/superUserDashboard/xlvask/pages/XLVaskUsageLogsPage.vue
index e9a85b94..bf6d7ac0 100644
--- a/src/views/dashboards/superUserDashboard/xlvask/pages/XLVaskUsageLogsPage.vue
+++ b/src/views/dashboards/superUserDashboard/xlvask/pages/XLVaskUsageLogsPage.vue
@@ -29,7 +29,7 @@ const fetchObjects = () => {
if (!isoDateFrom) {
Swal.fire({
icon: 'warning',
- title: t('invoicing_period.xlvask_autopilot.errors.load_usage_log_failed'),
+ title: t('invoicing_period.xlvask_review.errors.load_usage_log_failed'),
});
return;
}
@@ -43,14 +43,14 @@ const fetchObjects = () => {
console.error("XL-Vask: usage log list returned non-OK status.", { status: response?.status });
Swal.fire({
icon: 'error',
- title: t('invoicing_period.xlvask_autopilot.errors.load_usage_log_failed'),
+ title: t('invoicing_period.xlvask_review.errors.load_usage_log_failed'),
});
}
}).catch((error) => {
console.error("XL-Vask: failed to load usage log list.", { error });
Swal.fire({
icon: 'error',
- title: t('invoicing_period.xlvask_autopilot.errors.load_usage_log_failed'),
+ title: t('invoicing_period.xlvask_review.errors.load_usage_log_failed'),
});
}).finally(() => {
isLoading.value = false;
diff --git a/tests/e2e/invoicing-period.smoke.spec.js b/tests/e2e/invoicing-period.smoke.spec.js
index b807e458..c148bfea 100644
--- a/tests/e2e/invoicing-period.smoke.spec.js
+++ b/tests/e2e/invoicing-period.smoke.spec.js
@@ -1780,493 +1780,6 @@ test.describe("Invoicing period tab", () => {
]);
});
- test("@smoke period view opens Selvvask import and attaching view", async ({ page }, testInfo) => {
- const usageOrderRequests = [];
- const fastLinkRequests = [];
- const automationPreviewRequests = [];
- const automationApplyRequests = [];
- const adjudicationRequests = [];
- const importUsageRequests = [];
- let accepted = false;
- let actionHalted = false;
- let automation = {
- id: 7101,
- status: "suggested",
- action: "attach_order",
- confidence: 0.93,
- calibrated_probability: 0.995,
- source: "fuzzy",
- reason: "Samme primære produkt og relaterede tillæg matcher en ordre fra samme dag. Ordre #7001.",
- matched_order_id: 7001,
- created_order_id: null,
- candidate_order: {
- id: 7001,
- department_id: 1,
- created_at: "2026-03-10 12:03:00",
- },
- proposed_order: null,
- can_accept: true,
- can_deny: true,
- can_ignore: true,
- can_attach_order: true,
- can_create_order: true,
- review_eligible: true,
- evidence: ["Samme registrering og total"],
- contradictions: [],
- risk_flags: [],
- plan_steps: ["Kontrollér ordre #7001", "Tilknyt vasken atomisk"],
- candidate_orders: [{ order_id: 7001, reason: "Samme registrering og total" }],
- expected_version: "usage-v1",
- run_id: "run-history-1",
- policy_version: "policy-v1",
- model: "gpt-5.6-sol",
- };
- let autoAdjudication = {
- id: 7201,
- suggestion_id: 7201,
- status: "auto_accepted",
- action: "create_order",
- source: "openai",
- review_eligible: false,
- adjudication_eligible: true,
- allowed_adjudication_outcomes: ["correct", "incorrect", "duplicate", "cross_hall", "unaudited"],
- run_id: "run-auto-1",
- policy_version: "policy-v1",
- model: "gpt-5.6-sol",
- };
-
- await openPeriodView(page);
-
- await page.route("**/modules/xlvask/services/usage/automation/capabilities**", async (route) => {
- await route.fulfill(
- json({
- data: {
- can_view: true,
- can_review: true,
- can_dry_run: true,
- can_execute: !actionHalted,
- can_manage_policy: true,
- can_halt: true,
- effective_stage: actionHalted ? "halted" : "ai_attach_canary",
- allowed_modes: actionHalted ? ["dry_run"] : ["dry_run", "execute"],
- blocked_reasons: actionHalted ? ["automation_halted"] : [],
- effective_action_sources: ["deterministic", "openai"],
- readiness: {
- ready: !actionHalted,
- effective_stage: actionHalted ? "halted" : "ai_attach_canary",
- policy_version: "policy-v1",
- model: "gpt-5.6-sol",
- worker_healthy: true,
- budgets: {
- attach_order: { remaining_global: 100, remaining_hall: 10 },
- create_order: { remaining_global: 20, remaining_hall: 3 },
- },
- review_progress: {
- attach_order: { reviewed: actionHalted ? 41 : 40, target: 200 },
- create_order: { reviewed: 0, target: 50 },
- },
- eligible_counts: { attach_order: 1, create_order: 0, total: 1 },
- },
- },
- })
- );
- });
-
- await page.route("**/modules/xlvask/services/usage/automation/admin/readiness**", async (route) => {
- await route.fulfill(
- json({
- data: {
- ready: !actionHalted,
- effective_stage: actionHalted ? "halted" : "ai_attach_canary",
- policy_version: "policy-v1",
- model: "gpt-5.6-sol",
- worker_healthy: true,
- blocked_reasons: actionHalted ? ["automation_halted"] : [],
- budgets: {
- attach_order: { remaining_global: 100, remaining_hall: 10 },
- create_order: { remaining_global: 20, remaining_hall: 3 },
- },
- review_progress: {
- attach_order: { reviewed: actionHalted ? 41 : 40, target: 200 },
- create_order: { reviewed: 0, target: 50 },
- },
- eligible_counts: { attach_order: 1, create_order: 0, total: 1 },
- },
- })
- );
- });
-
- await page.route("**/modules/xlvask/services/usage/autopilot-runs**", async (route) => {
- const path = new URL(route.request().url()).pathname;
- if (route.request().method() === "GET" && path.endsWith("/autopilot-runs/active")) {
- await route.fulfill(json({ data: { run: null } }));
- return;
- }
- if (route.request().method() !== "POST" || !path.endsWith("/autopilot-runs")) {
- await route.fallback();
- return;
- }
- importUsageRequests.push(JSON.parse(route.request().postData() || "{}"));
- await route.fulfill(
- json({
- data: {
- run: {
- id: "run-1",
- status: "completed",
- phase: "completed",
- processed: 2,
- total: 2,
- },
- },
- })
- );
- });
-
- await page.route("**/modules/xlvask/services/usage/automation/decisions/**", async (route) => {
- const path = new URL(route.request().url()).pathname;
- const body = JSON.parse(route.request().postData() || "{}");
- if (path.endsWith("/preview")) {
- automationPreviewRequests.push(body);
- await route.fulfill(
- json({
- data: {
- preview: {
- id: "preview-1",
- selection_hash: "selection-1",
- action: body.action,
- items: [{ usage_log_id: 8101, before: {}, after: { linked_order_id: 7001 }, warnings: [] }],
- requires_confirmation: true,
- confirmation_phrase: "CONFIRM XLVASK",
- },
- },
- })
- );
- return;
- }
- if (path.endsWith("/apply")) {
- automationApplyRequests.push(body);
- accepted = true;
- automation = { ...automation, status: "accepted", can_accept: false, can_deny: false };
- await route.fulfill(json({ data: { applied: 1, results: [{ usage_log_id: 8101 }], failed: [] } }));
- return;
- }
- await route.fallback();
- });
-
- await page.route("**/modules/xlvask/services/usage/automation/admin/calibrations/labels**", async (route) => {
- const body = JSON.parse(route.request().postData() || "{}");
- adjudicationRequests.push(body);
- if (body.outcome === "correct") {
- autoAdjudication = {
- ...autoAdjudication,
- id: 7202,
- suggestion_id: 7202,
- allowed_adjudication_outcomes: ["incorrect"],
- };
- await route.fulfill(json({ data: { automatic_action_review: { outcome: "correct" } } }));
- return;
- }
- actionHalted = true;
- autoAdjudication = {
- ...autoAdjudication,
- adjudication_eligible: false,
- allowed_adjudication_outcomes: [],
- };
- await route.fulfill(json({ data: { action_halted: true, automatic_action_review: { outcome: body.outcome } } }));
- });
-
- await page.route("**/modules/xlvask/services/usage/orders**", async (route) => {
- if (route.request().method() !== "GET") {
- await route.fallback();
- return;
- }
-
- if (matchesApiPath(route.request().url(), "/modules/xlvask/services/usage/orders/fast-link")) {
- fastLinkRequests.push(route.request().url());
- await route.fulfill(
- json({
- data: {
- order_items: [
- {
- id: 9101,
- product_id: 301,
- product: { name: "Kassevogn/varevogn" },
- quantity: 1,
- price: 125,
- },
- ],
- potential_duplicates: [],
- },
- })
- );
- return;
- }
-
- if (matchesApiPath(route.request().url(), "/modules/xlvask/services/usage/orders/summary")) {
- await route.fulfill(
- json({
- data: {
- summary: {
- total: 3,
- new: 1,
- updated: 0,
- unchanged: 2,
- invalid: 0,
- already_linked: accepted ? 1 : 0,
- auto_linked: 0,
- auto_created: 1,
- needs_review: accepted ? 0 : 1,
- blocked: 0,
- ignored: 0,
- failed: 1,
- certain: accepted ? 2 : 1,
- uncertain: accepted ? 0 : 1,
- none: 1,
- },
- },
- })
- );
- return;
- }
-
- if (!matchesApiPath(route.request().url(), "/modules/xlvask/services/usage/orders")) {
- await route.fallback();
- return;
- }
-
- const url = new URL(route.request().url());
- usageOrderRequests.push({
- filters: url.searchParams.get("filters") || "",
- limit: url.searchParams.get("limit") || "",
- });
-
- await route.fulfill(
- json({
- data: [
- {
- id: 8101,
- reg_1: "AB12345",
- created_at: "2026-03-10 12:00:00",
- customer_id: 4001,
- customer_number: 4001,
- customer_name: "DEKRA AMU Center Hovedstaden A/S",
- department_id: 1,
- lane: 1,
- wash_id: "wash-selvvask-1",
- duplicates: [],
- fast_link_key: "temporary_cache_selfwash8101",
- total_net_amount: 125,
- xlvask_primary_product_name: "Kassevogn/varevogn",
- import_state: "new",
- resolution_state: accepted ? "already_linked" : "needs_review",
- certainty: accepted ? "certain" : "uncertain",
- planned_action: accepted ? "none" : "attach_order",
- automation,
- },
- {
- id: 8102,
- reg_1: "CD67890",
- created_at: "2026-03-10 12:15:00",
- customer_id: 4002,
- customer_number: 4002,
- customer_name: "Self Wash Transport",
- department_id: 2,
- lane: 2,
- wash_id: "wash-selvvask-2",
- duplicates: [],
- fast_link_key: null,
- total_net_amount: 88,
- xlvask_primary_product_name: "Varebil",
- import_state: "unchanged",
- resolution_state: "failed",
- certainty: "none",
- planned_action: "none",
- automation: {
- id: 7102,
- status: "failed",
- action: "attach_order",
- error: "No safe match",
- can_accept: false,
- can_deny: false,
- can_ignore: false,
- can_attach_order: false,
- can_create_order: false,
- review_eligible: false,
- },
- },
- {
- id: 8103,
- reg_1: "EF24680",
- created_at: "2026-03-10 12:30:00",
- customer_id: 4003,
- customer_number: 4003,
- customer_name: "Automatic Action Transport",
- department_id: 1,
- lane: 1,
- wash_id: "wash-selvvask-3",
- duplicates: [],
- fast_link_key: null,
- total_net_amount: 150,
- xlvask_primary_product_name: "Lastbil",
- import_state: "unchanged",
- resolution_state: "auto_created",
- certainty: "certain",
- planned_action: "none",
- automation: autoAdjudication,
- },
- ],
- meta: {
- pagination: {
- page: 1,
- per_page: 100,
- total: 3,
- },
- },
- })
- );
- });
-
- await setEntireMarchPeriod(page);
-
- await expect(page.getByTestId("invoicing-period-view-selector-self_wash")).toBeVisible();
- await expect(page.getByTestId("invoicing-period-view-selector-self_wash")).toContainText("Selvvask (1/3)");
- const selfWashProgress = page.getByTestId("invoicing-period-view-selector-progress-self_wash");
- const selfWashProgressMetrics = await selfWashProgress.evaluate((element) => {
- const rect = element.getBoundingClientRect();
- const slot = element.parentElement?.getBoundingClientRect();
- const controls = element.parentElement?.parentElement?.getBoundingClientRect();
- return {
- width: rect.width,
- height: rect.height,
- slotWidth: slot?.width || 0,
- controlsWidth: controls?.width || 0,
- };
- });
- expect(
- selfWashProgressMetrics.width,
- `self-wash selector progress geometry: ${JSON.stringify(selfWashProgressMetrics)}`
- ).toBeGreaterThanOrEqual(56);
- expect(selfWashProgressMetrics.height).toBeGreaterThan(0);
- await expect(selfWashProgress).toBeVisible();
-
- await page.getByTestId("invoicing-period-view-selector-self_wash").click();
- await expect(page.getByTestId("invoicing-period-self-wash-view")).toBeVisible();
- const selfWashView = page.getByTestId("invoicing-period-self-wash-view");
- await expect(selfWashView.getByTestId("date-period-start")).toHaveCount(0);
- await expect(selfWashView.locator("input[placeholder='Søg i transaktioner']")).toHaveCount(0);
- await expect(selfWashView.getByText("Per side")).toHaveCount(0);
- await expect(selfWashView.getByText("Rækkefølge")).toHaveCount(0);
- await expect(selfWashView.getByText("Dato fra")).toHaveCount(0);
- await expect(selfWashView.getByText("Dato til")).toHaveCount(0);
- await expect(selfWashView.getByText("Vis kun ikke tilknyttede vaske")).toHaveCount(0);
- await expect(page.getByRole("heading", { name: /Selvvask/ })).toBeVisible();
- await expect(page.getByText("AB12345")).toBeVisible();
- await expect(page.getByText("CD67890")).toBeVisible();
- if (await captureXlvaskVisualEvidence(page, testInfo)) {
- return;
- }
- await expect(page.getByTestId("xlvask-summary-new")).toContainText("1");
- await expect(page.getByTestId("xlvask-summary-unchanged")).toContainText("2");
- await expect(page.getByTestId("xlvask-summary-uncertain")).toContainText("1");
- await expect(page.getByTestId("xlvask-summary-failed")).toContainText("1");
- await expect(page.getByTestId("xlvask-automation-controls")).toBeVisible();
- await expect(page.getByTestId("xlvask-automation-readiness")).toContainText("policy-v1");
- await expect(selfWashView.locator(".xlvask-usage-price").first()).toContainText("125");
- await expect(page.getByTestId("xlvask-resolution-state-8102")).toContainText("Mislykket");
- await expect(page.getByTestId("xlvask-certainty-8102")).toContainText("Ikke vurderet");
- expect(fastLinkRequests).toHaveLength(0);
- await expect(page.getByTestId("xlvask-automation-suggestion-8101")).toContainText("Foreslået: Tilknyt ordre #7001");
- await expect(selfWashView.locator(".xlvask-usage-card-header").first()).toBeVisible();
-
- await page.getByText("EF24680").click();
- await page.getByTestId("xlvask-adjudication-correct-8103").click();
- await expect(page.getByRole("heading", { name: "Kontrollér det automatiske resultat" })).toBeVisible();
- await page.getByRole("button", { name: "Korrekt" }).click();
- await expect.poll(() => adjudicationRequests.length).toBe(1);
- expect(adjudicationRequests[0]).toEqual({ suggestion_id: 7201, outcome: "correct" });
- await expect(page.locator(".swal2-popup")).toHaveCount(0, { timeout: 4000 });
-
- await selfWashView.getByRole("checkbox", { name: "Vælg vask #8101" }).check();
- await expect(page.getByTestId("xlvask-autopilot-bulk-bar")).toContainText("1 vaske valgt");
- await page.getByTestId("xlvask-autopilot-bulk-bar").getByRole("button", { name: "Ryd valg" }).click();
- await expect(page.getByTestId("xlvask-autopilot-bulk-bar")).toHaveCount(0);
- await expect
- .poll(() => page.evaluate(() => document.documentElement.scrollWidth - document.documentElement.clientWidth))
- .toBeLessThanOrEqual(1);
-
- await page.getByText("AB12345").click();
- await expect(page.getByTestId("xlvask-automation-evidence-8101")).toContainText("Samme registrering og total");
- await expect(page.getByTestId("xlvask-automation-candidates-8101")).toContainText("#7001");
- await expect(page.getByTestId("xlvask-automation-accept-8101")).toBeVisible();
- await page.getByTestId("xlvask-automation-accept-8101").click();
- await expect(page.getByRole("heading", { name: "Kontrollér ændringen" })).toBeVisible();
- await page.locator(".swal2-input").fill("CONFIRM XLVASK");
- await page.getByRole("button", { name: "Udfør" }).click();
- await expect(page.getByTestId("xlvask-automation-suggestion-8101")).toContainText("Accepteret #7001");
- expect(automationPreviewRequests).toEqual([
- expect.objectContaining({
- usage_log_ids: [8101],
- action: "accept",
- suggestion_id: 7101,
- }),
- ]);
- expect(automationApplyRequests).toEqual([
- expect.objectContaining({
- preview_id: "preview-1",
- selection_hash: "selection-1",
- confirmation_text: "CONFIRM XLVASK",
- }),
- ]);
- await expect(page.getByTestId("invoicing-period-view-selector-self_wash")).toContainText("Selvvask (2/3)");
-
- await expect.poll(() => usageOrderRequests.length).toBeGreaterThan(0);
- expect(usageOrderRequests.some((request) => request.limit === "10000")).toBe(false);
- expect(
- usageOrderRequests.some(
- (request) =>
- request.filters.includes("StartTime-date_from:2026-03-01") &&
- request.filters.includes("StartTime-date_to:2026-03-31")
- )
- ).toBe(true);
- await selfWashView.getByTestId("xlvask-autopilot-dry-run").click();
- await expect.poll(() => importUsageRequests.length).toBe(1);
- expect(importUsageRequests[0]).toMatchObject({
- dateFrom: "2026-03-01",
- dateTo: "2026-03-31",
- mode: "dry_run",
- forceRefetch: true,
- });
- expect(importUsageRequests[0].idempotency_key).toEqual(expect.any(String));
-
- await selfWashView.getByTestId("xlvask-autopilot-execute").click();
- const executeDialog = page.locator(".swal2-popup");
- await expect(executeDialog).toContainText("2026-03-01");
- await expect(executeDialog).toContainText("deterministic, openai");
- await expect(executeDialog).toContainText("100 globalt; laveste restgrænse blandt haller 10");
- await expect(executeDialog).toContainText("20 globalt; laveste restgrænse blandt haller 3");
- await executeDialog.locator(".swal2-input").fill("KØR AUTOMATIK");
- await executeDialog.getByRole("button", { name: "Kør automatiske handlinger" }).click();
- await expect.poll(() => importUsageRequests.length).toBe(2);
- expect(importUsageRequests[1]).toMatchObject({ mode: "execute", forceRefetch: true });
- expect(importUsageRequests[1].idempotency_key).toEqual(expect.any(String));
- expect(importUsageRequests[1].idempotency_key).not.toBe(importUsageRequests[0].idempotency_key);
-
- const incorrectAdjudication = page.getByTestId("xlvask-adjudication-incorrect-8103");
- if (!(await incorrectAdjudication.isVisible())) {
- await page.getByText("EF24680").click();
- }
- await incorrectAdjudication.click();
- await expect(page.getByRole("heading", { name: "Kontrollér det automatiske resultat" })).toBeVisible();
- await page.getByRole("button", { name: "Forkert" }).click();
- await expect.poll(() => adjudicationRequests.length).toBe(2);
- expect(adjudicationRequests[1]).toEqual({ suggestion_id: 7202, outcome: "incorrect" });
- await expect(page.getByRole("heading", { name: "Resultatet er gemt, og automatikken er stoppet" })).toBeVisible();
- await expect(page.locator(".swal2-popup")).toHaveCount(0, { timeout: 4000 });
- await expect(selfWashView.getByTestId("xlvask-autopilot-execute")).toHaveCount(0);
- await expect(page.getByTestId("xlvask-automation-readiness")).toContainText("Ikke klar");
- expect(importUsageRequests.filter((request) => request.mode === "execute")).toHaveLength(1);
- });
-
test("@smoke period view shows flags and saves automatic flag decisions", async ({ page }) => {
const automaticStatusRequests = [];
const manualStatusRequests = [];
diff --git a/tests/unit/superuser-invoices-view.spec.js b/tests/unit/superuser-invoices-view.spec.js
index 33c35599..47fac268 100644
--- a/tests/unit/superuser-invoices-view.spec.js
+++ b/tests/unit/superuser-invoices-view.spec.js
@@ -591,10 +591,10 @@ describe("Periode tab contract", () => {
expect(xlvaskUsagePaginationSource).toContain('v-if="shouldShowLocalFilters"');
expect(xlvaskUsagePaginationSource).toContain('v-if="!hideSearchField && shouldShowLocalFilters"');
expect(xlvaskUsagePaginationSource).toContain('v-if="!props.loadAllAtOnce"');
- expect(xlvaskUsagePaginationSource).toContain("const buildImportUsageParams = () => {");
+ expect(xlvaskUsagePaginationSource).toContain("const buildUsagePaginationParams = (extra = {}) =>");
expect(xlvaskUsagePaginationSource).toContain("dateFrom: props.initialDateFrom");
expect(xlvaskUsagePaginationSource).toContain("dateTo: props.initialDateTo");
- expect(xlvaskUsagePaginationSource).toContain("buildImportUsageParams()");
+ expect(xlvaskUsagePaginationSource).toContain("buildUsagePaginationParams()");
});
it("loads Selvvask selector counts and progress from the selected period", () => {
@@ -619,86 +619,69 @@ describe("Periode tab contract", () => {
expect(xlvaskUsageOrdersTableSource).toContain("const { loadList } = usePaginatedListInstance();");
});
- it("wires the Selvvask view to the automation-workspace so operators see Accept / Reject / Ignore buttons", () => {
- // The Selvvask view (Superuser → Fakturaer → Periode → Selvvask) must enable
- // the automation workspace so the review actions surface in the orders table.
- expect(periodViewSelfWashSource).toContain(':automation-workspace="true"');
+ it("wires the Selvvask surface to operator-only accept / reject / ignore actions", () => {
+ // The Selvvask view (Superuser → Fakturaer → Periode → Selvvask) must
+ // exclusively surface the operator review actions (Accept / Reject /
+ // Ignore / Unignore). The autopilot / dry-run / execute / AI adjudication
+ // flows are no longer wired here — operator review is the whole surface.
+ expect(periodViewSelfWashSource).not.toContain(":automation-workspace=");
+ expect(periodViewSelfWashSource).not.toContain("can_review");
+ expect(periodViewSelfWashSource).not.toContain("can_manage_policy");
- // The pagination must forward `allow-review-actions` to the orders table using
- // the per-request capability flag, so operators with can_review=true get the
- // Accept / Reject / Ignore buttons. The AI-administrator-only flags
- // (can_dry_run / can_execute / can_manage_policy) must remain on automationWorkspace
- // directly so an operator without manage_xlvask_usage_automation cannot dry-run or
- // execute the autopilot pipeline.
- expect(xlvaskUsagePaginationSource).toContain(
- ':allow-review-actions="props.automationWorkspace && capabilities.can_review"'
- );
- expect(xlvaskUsagePaginationSource).toContain(
- ':allow-select-multiple="props.automationWorkspace && capabilities.can_review"'
- );
- expect(xlvaskUsagePaginationSource).toContain(
- ':allow-adjudication-actions="props.automationWorkspace && capabilities.can_manage_policy"'
- );
+ expect(xlvaskUsagePaginationSource).toContain(':allow-review-actions="true"');
+ expect(xlvaskUsagePaginationSource).toContain(':allow-select-multiple="true"');
+ expect(xlvaskUsagePaginationSource).toContain(':allow-adjudication-actions="false"');
- // The orders table must render the operator-facing action buttons
- // (Accept / Reject / Ignore / Audit) inside the right-hand action column,
- // gated only on `allowReviewActions` (i.e. the API capability, not on
- // manage_xlvask_usage_automation). This is the contract that lets the
- // /superuser/invoicing/.../?periodView=self_wash view actually accept and
- // edit XL Vask washes.
+ // The orders table exposes the per-row Accept / Reject / Ignore / Unignore
+ // buttons gated on `allowReviewActions`. The AI-adjudication buttons and
+ // the legacy "adjudication" tag must never appear in this surface.
expect(xlvaskUsageOrdersTableSource).toContain(
''
);
expect(xlvaskUsageOrdersTableSource).toContain(":data-testid=\"'xlvask-accept-' + object.id\"");
expect(xlvaskUsageOrdersTableSource).toContain(":data-testid=\"'xlvask-reject-' + object.id\"");
expect(xlvaskUsageOrdersTableSource).toContain(":data-testid=\"'xlvask-ignore-' + object.id\"");
-
- // Regression guard: the AI-adjudication row (adjudication) must remain gated
- // on allowAdjudicationActions (can_manage_policy) so a normal operator never
- // sees the calibration label buttons.
- expect(xlvaskUsageOrdersTableSource).toContain(':data-testid="`xlvask-adjudication-${outcome}-${object.id}`"');
+ expect(xlvaskUsageOrdersTableSource).not.toContain("xlvask-adjudication-");
});
it("keeps Selvvask usage entries wrapped inside the period content area", () => {
- expect(xlvaskUsageOrdersTableSource).toContain("xlvask-usage-card-primary");
- expect(xlvaskUsageOrdersTableSource).toContain("xlvask-usage-card-status");
- expect(xlvaskUsageOrdersTableSource).toContain("xlvask-usage-chip-text");
+ expect(xlvaskUsageOrdersTableSource).toContain("xlvask-review-table");
+ expect(xlvaskUsageOrdersTableSource).toContain("xlvask-usage-card");
expect(xlvaskUsageOrdersTableSource).toContain("getCachedXlvaskUsageAmount");
expect(xlvaskUsageOrdersTableSource).toContain("setCachedXlvaskUsageAmount");
expect(xlvaskUsageOrdersTableSource).toContain("object?.total_net_amount");
- expect(xlvaskUsageOrdersTableSource).toContain("automation.status === 'failed'");
- expect(xlvaskUsageOrdersTableSource).toContain("getXlvaskResolutionState(object)");
- expect(xlvaskUsageOrdersTableSource).toContain("only fetch one-time fast-link details on demand");
+ expect(xlvaskUsageOrdersTableSource).toContain("object.resolution_state");
+ expect(xlvaskUsageOrdersTableSource).toContain("onClickAccept(object)");
+ expect(xlvaskUsageOrdersTableSource).toContain("onClickReject(object)");
+ expect(xlvaskUsageOrdersTableSource).toContain("onClickIgnore(object)");
+ expect(xlvaskUsageOrdersTableSource).toContain("onClickUnignore(object)");
+ expect(xlvaskUsageOrdersTableSource).not.toContain("automation.status");
expect(xlvaskUsageOrdersTableSource).not.toContain("scheduleFastLink(object)");
- expect(xlvaskUsageOrdersTableSource).toContain("grid-template-columns: minmax(0, 1fr) minmax(9rem, 15rem);");
- expect(xlvaskUsageOrdersTableSource).toContain("xlvask-order-comparison-columns");
- expect(xlvaskUsageOrdersTableSource).toContain("@media screen and (max-width: 1350px)");
+ expect(xlvaskUsageOrdersTableSource).not.toContain("xlvask-order-comparison-columns");
});
- it("uses bounded autopilot polling and preview/apply-only review mutations", () => {
- expect(xlvaskUsagePaginationSource).toContain("const RUN_POLL_INTERVAL_MS = 2_000");
- expect(xlvaskUsagePaginationSource).toContain("const RUN_POLL_MAX_DURATION_MS = 5 * 60 * 1_000");
- expect(xlvaskUsagePaginationSource).toContain("document.hidden");
- expect(xlvaskUsagePaginationSource).toContain("resumeAutopilotPolling");
- expect(xlvaskUsagePaginationSource).toContain("invalidateAutopilotScope");
- expect(xlvaskUsagePaginationSource).toContain("if (sequence !== runPollSequence) return;");
- expect(xlvaskUsagePaginationSource).toContain("activeRunRecoverySequence += 1");
- expect(xlvaskUsagePaginationSource).toContain("await loadAutomationControlState();");
- expect(xlvaskUsagePaginationSource).toContain("await recoverActiveAutopilotRun();");
- expect(xlvaskUsagePaginationSource).toContain('halted: "advisory"');
- expect(xlvaskUsagePaginationSource).toContain("expected_policy_version: preview.expected_policy_version");
- expect(xlvaskUsagePaginationSource).toContain("buildImportUsageParams(),");
- expect(xlvaskUsagePaginationSource).toContain("const runParams = buildImportUsageParams();");
- expect(xlvaskUsagePaginationSource).toContain("const readinessSnapshot = buildExecutionReadinessSnapshot();");
- expect(xlvaskUsagePaginationSource).toContain("if (requestScopeSequence !== periodScopeSequence) return;");
- expect(xlvaskUsagePaginationSource).toContain("JSON.stringify(buildExecutionReadinessSnapshot())");
- expect(xlvaskUsagePaginationSource).toContain("if (!canStartExecute.value) return;");
- expect(xlvaskUsageOrdersTableSource).toContain('"/modules/xlvask/services/usage/automation/decisions/preview"');
- expect(xlvaskUsageOrdersTableSource).toContain('"/modules/xlvask/services/usage/automation/decisions/apply"');
- expect(xlvaskUsageOrdersTableSource).toContain("if (!confirmation.isConfirmed) return false;");
- expect(xlvaskUsageOrdersTableSource).toContain("if (applied) selectedUsageLogIds.value = [];");
- expect(xlvaskUsageOrdersTableSource).not.toContain("set.wash_id");
- expect(xlvaskUsageOrdersTableSource).not.toContain("'/order/items'");
+ it("uses summary refresh after operator review and stays on the review endpoints", () => {
+ // After an operator Accept / Reject / Ignore / Unignore, the pagination
+ // and the orders table must re-load the summary via the same review
+ // endpoints that this surface exclusively consumes. No autopilot / dry-run /
+ // apply / policy-version / readiness-snapshot plumbing survives.
+ expect(xlvaskUsagePaginationSource).toContain("/modules/xlvask/services/usage/orders/summary");
+ expect(xlvaskUsagePaginationSource).toContain("xlvask-usage-order-updated");
+ expect(xlvaskUsagePaginationSource).not.toContain("RUN_POLL_INTERVAL_MS");
+ expect(xlvaskUsagePaginationSource).not.toContain("RUN_POLL_MAX_DURATION_MS");
+ expect(xlvaskUsagePaginationSource).not.toContain("recoverActiveAutopilotRun");
+ expect(xlvaskUsagePaginationSource).not.toContain("loadAutomationControlState");
+ expect(xlvaskUsagePaginationSource).not.toContain("buildExecutionReadinessSnapshot");
+ expect(xlvaskUsagePaginationSource).not.toContain("resumeAutopilotPolling");
+ expect(xlvaskUsagePaginationSource).not.toContain("canStartExecute");
+
+ expect(xlvaskUsageOrdersTableSource).toContain("/modules/xlvask/services/usage/orders/${object.id}/");
+ expect(xlvaskUsageOrdersTableSource).toContain("/modules/xlvask/services/usage/orders/${id}/");
+ expect(xlvaskUsageOrdersTableSource).not.toContain("/modules/xlvask/services/usage/automation/decisions/preview");
+ expect(xlvaskUsageOrdersTableSource).not.toContain("/modules/xlvask/services/usage/automation/decisions/apply");
+ expect(xlvaskUsageOrdersTableSource).not.toContain("if (!confirmation.isConfirmed)");
+ expect(xlvaskUsageOrdersTableSource).not.toContain("'apply'");
+ expect(xlvaskUsageOrdersTableSource).not.toContain("'preview'");
});
it("keeps period subview navigation on valid keys", () => {
diff --git a/tests/unit/xlvask-autopilot-component.spec.js b/tests/unit/xlvask-autopilot-component.spec.js
deleted file mode 100644
index abda9ccf..00000000
--- a/tests/unit/xlvask-autopilot-component.spec.js
+++ /dev/null
@@ -1,220 +0,0 @@
-// @vitest-environment jsdom
-
-import { flushPromises, shallowMount } from "@vue/test-utils";
-import { beforeEach, describe, expect, it, vi } from "vitest";
-import { ref } from "vue";
-import { createI18n } from "vue-i18n";
-
-const { request, loadList, fire } = vi.hoisted(() => ({
- request: vi.fn(),
- loadList: vi.fn(),
- fire: vi.fn(),
-}));
-
-vi.mock("sweetalert2", () => ({ default: { fire } }));
-
-vi.mock("@/components/session/token/SessionUser.vue", () => ({
- SessionUser: {
- request,
- functions: {
- currency: { toLocal: (value) => String(value) },
- parseErrorMessage: (error) => String(error),
- },
- objects: {
- global: {
- language: {
- completed: "Completed",
- generated: "Generated",
- possible_duplicates: "Possible duplicates",
- price_match: "Price match",
- price_unmatch: "Price mismatch",
- regret: "Cancel",
- },
- },
- },
- },
-}));
-
-vi.mock("@/components/pagination/paginatedList.vue", () => ({
- usePaginatedListInstance: () => ({ loadList }),
-}));
-
-vi.mock("@/components/pagination/departmentTabs.vue", () => ({
- departments: ref([{ id: 1, name: "Hall 1" }]),
- getDepartments: vi.fn(),
- getDepartmentName: () => "Hall 1",
-}));
-
-vi.mock("@/components/displays/department/pos/sync/xlvaskUsageAmountCache.js", () => ({
- clearCachedXlvaskUsageAmount: vi.fn(),
- getCachedXlvaskUsageAmount: vi.fn(() => null),
- setCachedXlvaskUsageAmount: vi.fn(),
-}));
-
-import XlvaskUsageOrdersTable from "@/components/displays/department/pos/sync/xlvaskUsageOrdersTable.vue";
-
-const actionableRow = () => ({
- id: 8101,
- reg_1: "AB12345",
- created_at: new Date().toISOString(),
- customer_name: "Test customer",
- department_id: 1,
- lane: 1,
- total_net_amount: 125,
- import_state: "new",
- resolution_state: "needs_review",
- certainty: "certain",
- planned_action: "attach_order",
- automation: {
- id: 7101,
- status: "suggested",
- action: "attach_order",
- can_accept: true,
- can_deny: true,
- can_ignore: true,
- can_attach_order: true,
- can_create_order: true,
- review_eligible: true,
- },
-});
-
-const mountTable = (props = {}) =>
- shallowMount(XlvaskUsageOrdersTable, {
- props: {
- objects: [actionableRow()],
- ...props,
- },
- global: {
- plugins: [
- createI18n({ legacy: false, locale: "en", missingWarn: false, fallbackWarn: false, messages: { en: {} } }),
- ],
- stubs: {
- WhiteBoxCard: {
- template: "",
- },
- OrderItemsTable: true,
- },
- },
- });
-
-describe("XL-Vask automation component authorization", () => {
- beforeEach(() => {
- request.mockReset();
- request.mockResolvedValue({ data: { data: { action_halted: false } } });
- loadList.mockReset();
- fire.mockReset();
- fire.mockResolvedValue({ isConfirmed: true });
- });
-
- it("renders evidence read-only when review capability is absent", () => {
- const wrapper = mountTable({ allowSelectMultiple: true, allowReviewActions: false });
-
- expect(wrapper.find("input[type='checkbox']").exists()).toBe(false);
- expect(wrapper.find("[data-testid='xlvask-automation-accept-8101']").exists()).toBe(false);
- expect(wrapper.find(".xlvask-usage-actions-column").exists()).toBe(false);
- });
-
- it("renders mutation controls only for an eligible row with review capability", () => {
- const wrapper = mountTable({ allowSelectMultiple: true, allowReviewActions: true, allowAdjudicationActions: true });
-
- expect(wrapper.find("input[type='checkbox']").attributes("disabled")).toBeUndefined();
- expect(wrapper.find("[data-testid='xlvask-automation-accept-8101']").exists()).toBe(true);
- expect(wrapper.find(".xlvask-usage-actions-column").exists()).toBe(true);
- });
-
- it("keeps an ineligible row unselectable", () => {
- const row = actionableRow();
- row.automation.review_eligible = false;
- const wrapper = mountTable({ objects: [row], allowSelectMultiple: true, allowReviewActions: true });
-
- expect(wrapper.find("input[type='checkbox']").attributes("disabled")).toBeDefined();
- });
-
- it("does not infer eligibility or action permissions from suggested status", async () => {
- const row = actionableRow();
- delete row.automation.review_eligible;
- row.automation.can_accept = false;
- row.automation.can_attach_order = false;
- row.automation.candidate_orders = [{ order_id: 7001 }];
- const wrapper = mountTable({ objects: [row], allowSelectMultiple: true, allowReviewActions: true });
-
- expect(wrapper.find("input[type='checkbox']").attributes("disabled")).toBeDefined();
- expect(wrapper.find("[data-testid='xlvask-automation-accept-8101']").exists()).toBe(false);
- expect(
- wrapper.find("[data-testid='xlvask-automation-candidates-8101'] button").attributes("disabled")
- ).toBeDefined();
- await wrapper.find("[data-testid='xlvask-automation-candidates-8101'] button").trigger("click");
- expect(request).not.toHaveBeenCalled();
- });
-
- it("makes the department read-only rendering issue no mutation request", async () => {
- const wrapper = mountTable({ allowSelectMultiple: true, allowReviewActions: false });
- await wrapper.find(".xlvask-usage-registration").trigger("click");
- expect(request).not.toHaveBeenCalled();
- });
-
- it("submits an explicitly allowed post-action outcome only for policy managers", async () => {
- const row = actionableRow();
- row.automation = {
- id: 7201,
- suggestion_id: 7201,
- status: "auto_accepted",
- adjudication_eligible: true,
- allowed_adjudication_outcomes: ["correct", "incorrect", "not_allowed"],
- };
- const wrapper = mountTable({
- objects: [row],
- allowReviewActions: false,
- allowAdjudicationActions: true,
- });
-
- expect(wrapper.find("[data-testid='xlvask-adjudication-correct-8101']").exists()).toBe(true);
- expect(wrapper.find("[data-testid='xlvask-adjudication-not_allowed-8101']").exists()).toBe(false);
- await wrapper.find("[data-testid='xlvask-adjudication-correct-8101']").trigger("click");
- await flushPromises();
-
- expect(request).toHaveBeenCalledWith(
- "/modules/xlvask/services/usage/automation/admin/calibrations/labels",
- "POST",
- { suggestion_id: 7201, outcome: "correct" }
- );
- expect(loadList).toHaveBeenCalled();
- });
-
- it("does not expose adjudication to an ordinary reviewer", () => {
- const row = actionableRow();
- row.automation.adjudication_eligible = true;
- row.automation.allowed_adjudication_outcomes = ["correct"];
- const wrapper = mountTable({
- objects: [row],
- allowReviewActions: true,
- allowAdjudicationActions: false,
- });
-
- expect(wrapper.find("[data-testid='xlvask-adjudication-8101']").exists()).toBe(false);
- });
-
- it("surfaces every successful adverse adjudication as an automation halt", async () => {
- const row = actionableRow();
- row.automation = {
- id: 7202,
- status: "auto_accepted",
- adjudication_eligible: true,
- allowed_adjudication_outcomes: ["incorrect"],
- };
- const wrapper = mountTable({
- objects: [row],
- allowAdjudicationActions: true,
- });
-
- await wrapper.find("[data-testid='xlvask-adjudication-incorrect-8101']").trigger("click");
- await flushPromises();
-
- expect(fire).toHaveBeenLastCalledWith(
- expect.objectContaining({
- icon: "warning",
- title: "invoicing_period.xlvask_autopilot.adjudication.halted",
- })
- );
- });
-});
diff --git a/tests/unit/xlvask-autopilot-ui.spec.js b/tests/unit/xlvask-autopilot-ui.spec.js
deleted file mode 100644
index 508de12b..00000000
--- a/tests/unit/xlvask-autopilot-ui.spec.js
+++ /dev/null
@@ -1,158 +0,0 @@
-import { describe, expect, it } from "vitest";
-
-import {
- getXlvaskCalibratedProbability,
- getXlvaskCertainty,
- getXlvaskImportState,
- getXlvaskPlannedAction,
- getXlvaskResolutionState,
- isXlvaskAutopilotRunActive,
- isXlvaskReviewEligible,
- normalizeXlvaskAutomationCapabilities,
- normalizeXlvaskAutomationReadiness,
- normalizeXlvaskAutopilotRun,
- normalizeXlvaskAutopilotSummary,
- xlvaskAutopilotRunProgress,
- xlvaskCandidateLabel,
-} from "@/components/displays/department/pos/sync/xlvaskAutopilotUi.js";
-
-describe("XL-Vask autopilot UI contract", () => {
- it("normalizes missing and invalid summary counts", () => {
- expect(normalizeXlvaskAutopilotSummary({ total: "12", failed: -2, blocked: "3.8" })).toMatchObject({
- total: 12,
- failed: 0,
- blocked: 3,
- auto_created: 0,
- uncertain: 0,
- });
- });
-
- it("keeps import, resolution, certainty, and planned action independent", () => {
- const row = {
- import_state: "updated",
- resolution_state: "blocked",
- certainty: "uncertain",
- planned_action: "resolve_mapping",
- automation: { calibrated_probability: 0.73 },
- };
-
- expect(getXlvaskImportState(row)).toBe("updated");
- expect(getXlvaskResolutionState(row)).toBe("blocked");
- expect(getXlvaskCertainty(row)).toBe("uncertain");
- expect(getXlvaskPlannedAction(row)).toBe("resolve_mapping");
- expect(getXlvaskCalibratedProbability(row)).toBe(0.73);
- });
-
- it("keeps legacy failed and no-safe-match rows visible through fallback states", () => {
- expect(getXlvaskResolutionState({ automation: { status: "failed" } })).toBe("failed");
- expect(getXlvaskResolutionState({ automation: { status: "none" } })).toBe("needs_review");
- expect(getXlvaskCertainty({ automation: { status: "none" } })).toBe("none");
- });
-
- it("never labels raw model confidence as a calibrated probability", () => {
- expect(getXlvaskCalibratedProbability({ automation: { confidence: 0.99 } })).toBeNull();
- });
-
- it("normalizes active run progress without exceeding 100 percent", () => {
- const run = normalizeXlvaskAutopilotRun({
- id: "run-1",
- status: "running",
- processed: 12,
- total: 10,
- });
-
- expect(isXlvaskAutopilotRunActive(run)).toBe(true);
- expect(isXlvaskAutopilotRunActive({ status: "retry_wait" })).toBe(true);
- expect(xlvaskAutopilotRunProgress(run)).toBe(100);
- expect(isXlvaskAutopilotRunActive({ status: "completed" })).toBe(false);
- expect(isXlvaskAutopilotRunActive({ status: "completed_with_warnings" })).toBe(false);
- });
-
- it("formats candidate order labels without trusting one wire shape", () => {
- expect(xlvaskCandidateLabel({ order_id: 7001, reason: "Same plate and total" })).toBe(
- "#7001 · Same plate and total"
- );
- expect(xlvaskCandidateLabel({ id: 7002 })).toBe("#7002");
- });
-
- it("normalizes capabilities fail closed", () => {
- expect(
- normalizeXlvaskAutomationCapabilities({
- can_view: true,
- can_review: true,
- allowed_modes: ["dry_run", "execute"],
- effective_action_sources: ["deterministic", "openai"],
- })
- ).toMatchObject({
- can_view: true,
- can_review: true,
- can_execute: false,
- allowed_modes: ["dry_run", "execute"],
- effective_action_sources: ["deterministic", "openai"],
- });
- });
-
- it("normalizes readiness budgets and fixed review targets", () => {
- expect(
- normalizeXlvaskAutomationReadiness({
- ready: true,
- review_progress: { attach_order: { reviewed: 12 }, create_order: { reviewed: 3 } },
- budgets: { attach_order: { remaining_global: 80, remaining_hall: 8 } },
- eligible_counts: { attach_order: 6, create_order: 2 },
- })
- ).toMatchObject({
- ready: true,
- review_progress: {
- attach_order: { reviewed: 12, target: 200 },
- create_order: { reviewed: 3, target: 50 },
- },
- budgets: { attach_order: { remaining_global: 80, remaining_hall: 8 } },
- eligible_counts: { attach_order: 6, create_order: 2, total: 8 },
- });
- });
-
- it("accepts only explicit eligible review rows when the backend supplies the flag", () => {
- expect(
- isXlvaskReviewEligible({
- resolution_state: "needs_review",
- automation: { id: 1, review_eligible: true },
- })
- ).toBe(true);
- expect(
- isXlvaskReviewEligible({
- resolution_state: "needs_review",
- automation: { id: 1, review_eligible: false },
- })
- ).toBe(false);
- expect(
- isXlvaskReviewEligible({
- resolution_state: "needs_review",
- automation: { id: 1, status: "suggested", can_accept: true },
- })
- ).toBe(false);
- expect(
- isXlvaskReviewEligible({
- resolution_state: "needs_review",
- automation: { id: 1, eligible: true },
- })
- ).toBe(false);
- });
-
- it("treats malformed capabilities as denied", () => {
- expect(
- normalizeXlvaskAutomationCapabilities({
- can_view: "true",
- can_review: 1,
- can_execute: "yes",
- allowed_modes: "execute",
- blocked_reasons: { reason: "bad" },
- })
- ).toMatchObject({
- can_view: false,
- can_review: false,
- can_execute: false,
- allowed_modes: [],
- blocked_reasons: [],
- });
- });
-});
diff --git a/tests/unit/xlvask-manual-review.spec.js b/tests/unit/xlvask-manual-review.spec.js
deleted file mode 100644
index d90b4c58..00000000
--- a/tests/unit/xlvask-manual-review.spec.js
+++ /dev/null
@@ -1,203 +0,0 @@
-// @vitest-environment jsdom
-
-import { flushPromises, shallowMount } from "@vue/test-utils";
-import { beforeEach, describe, expect, it, vi } from "vitest";
-import { ref } from "vue";
-import { createI18n } from "vue-i18n";
-
-const { request, loadList, fire } = vi.hoisted(() => ({
- request: vi.fn(),
- loadList: vi.fn(),
- fire: vi.fn(),
-}));
-
-vi.mock("sweetalert2", () => ({ default: { fire } }));
-
-vi.mock("@/components/session/token/SessionUser.vue", () => ({
- SessionUser: {
- request,
- functions: {
- currency: { toLocal: (value) => String(value) },
- parseErrorMessage: (error) => String(error),
- },
- objects: {
- global: {
- language: {
- completed: "Completed",
- generated: "Generated",
- possible_duplicates: "Possible duplicates",
- price_match: "Price match",
- price_unmatch: "Price mismatch",
- },
- },
- orders: { meta: { title: "Orders" } },
- },
- },
-}));
-
-vi.mock("@/components/pagination/paginatedList.vue", () => ({
- usePaginatedListInstance: () => ({ loadList }),
-}));
-
-vi.mock("@/components/pagination/departmentTabs.vue", () => ({
- departments: ref([{ id: 1, name: "Hall 1" }]),
- getDepartments: vi.fn(),
- getDepartmentName: () => "Hall 1",
-}));
-
-vi.mock("@/components/displays/department/pos/sync/xlvaskUsageAmountCache.js", () => ({
- clearCachedXlvaskUsageAmount: vi.fn(),
- getCachedXlvaskUsageAmount: vi.fn(() => null),
- setCachedXlvaskUsageAmount: vi.fn(),
-}));
-
-import XlvaskUsageOrdersTable from "@/components/displays/department/pos/sync/xlvaskUsageOrdersTable.vue";
-
-const reviewEligibleRow = (overrides = {}) => ({
- id: 9101,
- reg_1: "ZZ99001",
- created_at: new Date().toISOString(),
- customer_name: "Test customer",
- department_id: 1,
- lane: 1,
- total_net_amount: 250,
- import_state: "new",
- resolution_state: "needs_review",
- certainty: "uncertain",
- planned_action: "none",
- // No AI suggestion yet — automation is empty (the autopilot hasn't run).
- automation: {
- status: "none",
- review_eligible: true,
- },
- ...overrides,
-});
-
-const mountTable = (props = {}) =>
- shallowMount(XlvaskUsageOrdersTable, {
- props: {
- objects: [reviewEligibleRow()],
- allowReviewActions: true,
- allowSelectMultiple: true,
- ...props,
- },
- global: {
- plugins: [
- createI18n({ legacy: false, locale: "en", missingWarn: false, fallbackWarn: false, messages: { en: {} } }),
- ],
- stubs: {
- WhiteBoxCard: {
- template: "",
- },
- OrderItemsTable: true,
- },
- },
- });
-
-describe("XL-Vask manual review buttons", () => {
- beforeEach(() => {
- request.mockReset();
- loadList.mockReset();
- fire.mockReset();
- });
-
- it("exposes accept / reject / ignore buttons even when the autopilot has no suggestion", () => {
- const wrapper = mountTable();
-
- expect(wrapper.find("[data-testid='xlvask-accept-9101']").exists()).toBe(true);
- expect(wrapper.find("[data-testid='xlvask-reject-9101']").exists()).toBe(true);
- expect(wrapper.find("[data-testid='xlvask-ignore-9101']").exists()).toBe(true);
- });
-
- it("sends force_manual=true when accepting without an AI suggestion", async () => {
- const wrapper = mountTable();
-
- request.mockResolvedValue({
- data: {
- data: {
- preview: { id: "preview-id", selection_hash: "abc12345".repeat(8) },
- },
- },
- });
-
- await wrapper.find("[data-testid='xlvask-accept-9101']").trigger("click");
- await flushPromises();
-
- const previewCall = request.mock.calls.find(
- ([endpoint, method]) =>
- endpoint === "/modules/xlvask/services/usage/automation/decisions/preview" && method === "POST"
- );
- expect(previewCall).toBeDefined();
- const payload = previewCall[2];
- expect(payload).toMatchObject({
- usage_log_ids: [9101],
- action: "create_order",
- force_manual: true,
- });
- });
-
- it("does not send force_manual when the AI already has a matching suggestion", async () => {
- const row = reviewEligibleRow({
- automation: {
- id: 5001,
- status: "suggested",
- action: "create_order",
- can_accept: true,
- can_deny: true,
- can_ignore: true,
- can_attach_order: true,
- can_create_order: true,
- review_eligible: true,
- },
- });
- const wrapper = mountTable({ objects: [row] });
-
- request.mockResolvedValue({
- data: {
- data: {
- preview: { id: "preview-id-2", selection_hash: "def67890".repeat(8) },
- },
- },
- });
-
- await wrapper.find("[data-testid='xlvask-accept-9101']").trigger("click");
- await flushPromises();
-
- const previewCall = request.mock.calls.find(
- ([endpoint, method]) =>
- endpoint === "/modules/xlvask/services/usage/automation/decisions/preview" && method === "POST"
- );
- expect(previewCall).toBeDefined();
- const payload = previewCall[2];
- expect(payload.action).toBe("create_order");
- expect(payload).not.toHaveProperty("force_manual");
- });
-
- it("hides the buttons once the row is already resolved (no double-action)", () => {
- const row = reviewEligibleRow({
- resolution_state: "auto_linked",
- automation: { status: "accepted", action: "attach_order", review_eligible: false },
- });
- const wrapper = mountTable({ objects: [row] });
-
- expect(wrapper.find("[data-testid='xlvask-accept-9101']").exists()).toBe(false);
- expect(wrapper.find("[data-testid='xlvask-reject-9101']").exists()).toBe(false);
- expect(wrapper.find("[data-testid='xlvask-ignore-9101']").exists()).toBe(false);
- });
-
- it("surfaces the backend error when the manual review is rejected", async () => {
- const wrapper = mountTable();
-
- request.mockRejectedValue(new Error("Wash-id uniqueness activation is blocked"));
-
- await wrapper.find("[data-testid='xlvask-accept-9101']").trigger("click");
- await flushPromises();
-
- expect(fire).toHaveBeenCalledWith(
- expect.objectContaining({
- icon: "error",
- title: "invoicing_period.xlvask_autopilot.preview.error_title",
- })
- );
- });
-});
diff --git a/tests/unit/xlvask-usage-filters.spec.js b/tests/unit/xlvask-usage-filters.spec.js
index ac05a53d..ff0d90b8 100644
--- a/tests/unit/xlvask-usage-filters.spec.js
+++ b/tests/unit/xlvask-usage-filters.spec.js
@@ -33,26 +33,26 @@ describe("xlvask usage filters", () => {
expect(filterUsageOrdersByAttachment(sourceRows, false)).toEqual(sourceRows);
});
- it("treats accepted automation actions as attached", () => {
+ it("treats linked and duplicate wash_ids as attached", () => {
expect(
isUsageOrderAttachedToOrder({
wash_id: "wash-1",
+ linked_order_id: 17,
duplicates: [],
- automation: {
- status: "accepted",
- action: "attach_order",
- },
})
).toBe(true);
expect(
isUsageOrderAttachedToOrder({
wash_id: "wash-2",
+ duplicates: [{ wash_id: "wash-2" }],
+ })
+ ).toBe(true);
+
+ expect(
+ isUsageOrderAttachedToOrder({
+ wash_id: "wash-3",
duplicates: [],
- automation: {
- status: "denied",
- action: "attach_order",
- },
})
).toBe(false);
});