Replace unreviewed dynamic invoice-period translation calls with a bounded literal-key contract and focused fallback coverage.
151 lines
6.8 KiB
JavaScript
151 lines
6.8 KiB
JavaScript
import { describe, expect, it } from "vitest";
|
|
import { readFileSync } from "node:fs";
|
|
import { fileURLToPath } from "node:url";
|
|
import { buildPeriodCacheKey } from "../../src/views/dashboards/superUserDashboard/InvoicingBillingPeriod/imports/InvoicingBillingPeriodImportPaging.js";
|
|
import { deriveLegacyPeriodCustomerReview } from "../../src/views/dashboards/superUserDashboard/InvoicingBillingPeriod/services/invoicingPeriodReview.js";
|
|
import { translateInvoicingPeriodReview } from "../../src/views/dashboards/superUserDashboard/InvoicingBillingPeriod/services/invoicingPeriodTranslation.js";
|
|
|
|
const readProjectFile = (path) => readFileSync(fileURLToPath(new URL(`../../${path}`, import.meta.url)), "utf8");
|
|
|
|
describe("invoicing period review workspace", () => {
|
|
it("translates only the bounded review-workspace key contract", () => {
|
|
const calls = [];
|
|
const composer = {
|
|
t(key, params) {
|
|
calls.push([key, params]);
|
|
return key === "invoicing_period.review_workspace.queue.count" ? `${params.count} kunder` : key;
|
|
},
|
|
};
|
|
|
|
expect(translateInvoicingPeriodReview(composer, "queue.count", "fallback", { count: 3 })).toBe("3 kunder");
|
|
expect(translateInvoicingPeriodReview(composer, "queue.unknown", "fallback")).toBe("fallback");
|
|
expect(translateInvoicingPeriodReview(composer, "../queue.count", "fallback")).toBe("fallback");
|
|
expect(calls).toEqual([["invoicing_period.review_workspace.queue.count", { count: 3 }]]);
|
|
});
|
|
|
|
it("scopes cached pages by every server-side review and sorting input", () => {
|
|
const base = {
|
|
dateFrom: "2026-08-01",
|
|
dateTo: "2026-08-31",
|
|
periodView: "all",
|
|
page: 1,
|
|
limit: 100,
|
|
search: "",
|
|
flagTab: "all",
|
|
includeRequiresAction: true,
|
|
includeBooked: true,
|
|
reviewState: "blocked",
|
|
severity: "red",
|
|
invoiceState: "draft",
|
|
departmentId: "7",
|
|
sort: "priority",
|
|
direction: "asc",
|
|
};
|
|
|
|
expect(buildPeriodCacheKey(base)).not.toBe(buildPeriodCacheKey({ ...base, reviewState: "ready" }));
|
|
expect(buildPeriodCacheKey(base)).not.toBe(buildPeriodCacheKey({ ...base, sort: "total_amount" }));
|
|
expect(buildPeriodCacheKey(base)).not.toBe(buildPeriodCacheKey({ ...base, direction: "desc" }));
|
|
});
|
|
|
|
it("keeps selected-customer restoration route-only while review inputs reload the server page", () => {
|
|
const source = readProjectFile(
|
|
"src/views/dashboards/superUserDashboard/InvoicingBillingPeriod/displays/layout/Right.vue"
|
|
);
|
|
|
|
expect(source).toContain("reviewState: periodPaging.reviewState");
|
|
expect(source).toContain("departmentId: periodPaging.departmentId");
|
|
expect(source).toContain("sort: periodPaging.sort");
|
|
expect(source).toContain("() => periodPaging.selectedCustomerNumber");
|
|
expect(source).toContain("syncPeriodRouteQuery();");
|
|
expect(source).not.toMatch(/periodPaging\.direction,\s*periodPaging\.selectedCustomerNumber/);
|
|
});
|
|
|
|
it("exposes a dedicated keyboard selection control, explicit states, and two-pane review semantics", () => {
|
|
const source = readProjectFile(
|
|
"src/views/dashboards/superUserDashboard/InvoicingBillingPeriod/views/InvoicingBillingPeriodViewAll.vue"
|
|
);
|
|
|
|
expect(source).toContain('class="period-review-workspace"');
|
|
expect(source).toContain('role="group"');
|
|
expect(source).toContain('data-testid="`invoicing-period-customer-select-${customer.customer_number}`"');
|
|
expect(source).toContain(':aria-pressed="selectedCustomer === customer"');
|
|
expect(source).not.toContain('@keydown.enter.prevent="onClickCustomer(customer)"');
|
|
expect(source).toContain('data-testid="invoicing-period-initial-loading"');
|
|
expect(source).toContain('data-testid="invoicing-period-filtered-empty"');
|
|
expect(source).toContain('data-testid="invoicing-period-retained-error"');
|
|
});
|
|
|
|
it.each([
|
|
{
|
|
name: "manual flags take blocking precedence",
|
|
customer: { invoice_collections: [{ error_message: "failed" }], transactions: [] },
|
|
flags: { manualFlags: 2 },
|
|
expected: ["blocked", "resolve_manual_flags", ["manual_flags", "collection_errors"]],
|
|
},
|
|
{
|
|
name: "collection errors block",
|
|
customer: { invoice_collections: [{ error_message: "failed" }], transactions: [] },
|
|
flags: {},
|
|
expected: ["blocked", "resolve_collection_errors", ["collection_errors"]],
|
|
},
|
|
{
|
|
name: "drafts block with the backend reason code",
|
|
customer: { draft: { is_action_blocked: true }, transactions: [] },
|
|
flags: {},
|
|
expected: ["blocked", "resolve_draft", ["draft_blocks_action"]],
|
|
},
|
|
{
|
|
name: "requires action only means attention when no transaction is unbooked",
|
|
customer: { requires_action: true, transactions: [] },
|
|
flags: {},
|
|
expected: ["attention", "review_warnings", ["requires_action"]],
|
|
},
|
|
{
|
|
name: "automatic warnings outrank an active export",
|
|
customer: { queue: { has_active_job: true, invoice_collection_ids: [1] }, transactions: [] },
|
|
flags: { automaticFlags: 1 },
|
|
expected: ["attention", "review_warnings", ["automatic_warnings", "export_in_progress"]],
|
|
},
|
|
{
|
|
name: "an active export queues otherwise ready work",
|
|
customer: {
|
|
queue: { has_active_job: true, invoice_collection_ids: [1, 2] },
|
|
transactions: [{ booked: false, excluded: false }],
|
|
},
|
|
flags: {},
|
|
expected: ["queued", "wait_for_export", ["export_in_progress", "unbooked_transactions"]],
|
|
},
|
|
{
|
|
name: "only booked work is completed",
|
|
customer: { transactions: [{ booked: true, excluded: false }] },
|
|
flags: {},
|
|
expected: ["completed", "none", []],
|
|
},
|
|
{
|
|
name: "unbooked work remains ready even when requires action is set",
|
|
customer: { requires_action: true, transactions: [{ booked: false, excluded: false }] },
|
|
flags: {},
|
|
expected: ["ready", "create_invoice", ["unbooked_transactions"]],
|
|
},
|
|
{
|
|
name: "an empty customer is ready rather than completed",
|
|
customer: { transactions: [] },
|
|
flags: {},
|
|
expected: ["ready", "create_invoice", []],
|
|
},
|
|
])("matches the backend fallback matrix: $name", ({ customer, flags, expected }) => {
|
|
const review = deriveLegacyPeriodCustomerReview(customer, flags);
|
|
expect([review.state, review.next_action, review.reasons.map((reason) => reason.code)]).toEqual(expected);
|
|
});
|
|
|
|
it("uses durable backend queue jobs and keeps browser enqueueing only as a legacy fallback", () => {
|
|
const source = readProjectFile(
|
|
"src/views/dashboards/superUserDashboard/InvoicingBillingPeriod/components/InvoicingPeriodObjectTree.vue"
|
|
);
|
|
|
|
expect(source).toContain("const wasDurablyQueuedByServer");
|
|
expect(source).toContain("applied?.queue_job_ids ?? applied?.result?.queue_job_ids");
|
|
expect(source).toMatch(/if \(!wasDurablyQueuedByServer\) \{[\s\S]*addInvoiceCollectionsToQueue/);
|
|
});
|
|
});
|