- Introduced `.prettierrc.json` to enforce consistent code formatting across the project. - Updated unit and e2e test files to address formatting issues, improve readability, and ensure alignment with the new Prettier configuration.
711 lines
20 KiB
JavaScript
711 lines
20 KiB
JavaScript
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
import { useEconomicQueueJob } from "@/composables/useEconomicQueueJob.js";
|
|
import {
|
|
buildCollectedInvoiceEconomicPayload,
|
|
parseEconomicTransferQueueListResponse,
|
|
parseQueueJobFromEnqueueResponse,
|
|
parseQueueRunResponse,
|
|
parseQueueJobFromRetryResponse,
|
|
} from "@/services/economicTransferQueue.js";
|
|
|
|
const createQueueJobResponse = (job) => ({
|
|
data: {
|
|
data: {
|
|
job,
|
|
},
|
|
},
|
|
});
|
|
|
|
const createQueueStatusResponse = (job) => ({
|
|
data: {
|
|
data: job,
|
|
},
|
|
});
|
|
|
|
describe("economic transfer queue workflow", () => {
|
|
beforeEach(() => {
|
|
vi.useRealTimers();
|
|
});
|
|
|
|
afterEach(() => {
|
|
vi.useRealTimers();
|
|
});
|
|
|
|
it("handles enqueue success then polling to completed", async () => {
|
|
vi.useFakeTimers();
|
|
|
|
const requestFn = vi.fn();
|
|
let statusCallCount = 0;
|
|
|
|
requestFn.mockImplementation(async (endpoint, method, payload) => {
|
|
if (endpoint === "/economic/invoice/export" && method === "POST") {
|
|
expect(payload).toEqual({ order_id: 91 });
|
|
return createQueueJobResponse({
|
|
id: 1001,
|
|
status: "QUEUED",
|
|
progress_percent: 0,
|
|
progress_message: "Queued for export",
|
|
});
|
|
}
|
|
|
|
if (endpoint === "/economic/invoice/export/status" && method === "GET") {
|
|
expect(payload).toEqual({ job_id: 1001 });
|
|
statusCallCount += 1;
|
|
|
|
if (statusCallCount === 1) {
|
|
return createQueueStatusResponse({
|
|
id: 1001,
|
|
status: "PROCESSING",
|
|
progress_percent: 35,
|
|
progress_message: "Collecting invoice data",
|
|
});
|
|
}
|
|
|
|
return createQueueStatusResponse({
|
|
id: 1001,
|
|
status: "COMPLETED",
|
|
progress_percent: 100,
|
|
progress_message: "Completed",
|
|
result: {
|
|
message: "Invoice exported",
|
|
},
|
|
});
|
|
}
|
|
|
|
throw new Error(`Unexpected request ${method} ${endpoint}`);
|
|
});
|
|
|
|
const queue = useEconomicQueueJob({
|
|
enqueueEndpoint: "/economic/invoice/export",
|
|
statusEndpoint: "/economic/invoice/export/status",
|
|
retryEndpoint: "/economic/invoice/export/retry",
|
|
requestFn,
|
|
pollIntervalMs: 250,
|
|
buildEnqueuePayload: (payload) => ({
|
|
order_id: Number.parseInt(String(payload.order_id), 10),
|
|
}),
|
|
});
|
|
|
|
const enqueueResult = await queue.enqueue({ order_id: 91 });
|
|
|
|
expect(enqueueResult.id).toBe(1001);
|
|
expect(queue.status.value).toBe("PROCESSING");
|
|
expect(queue.isQueuedOrProcessing.value).toBe(true);
|
|
expect(queue.disableSubmit.value).toBe(true);
|
|
expect(queue.transportErrorMessage.value).toBe("");
|
|
|
|
await vi.advanceTimersByTimeAsync(250);
|
|
|
|
expect(queue.status.value).toBe("COMPLETED");
|
|
expect(queue.isCompleted.value).toBe(true);
|
|
expect(queue.isQueuedOrProcessing.value).toBe(false);
|
|
expect(queue.result.value).toEqual({ message: "Invoice exported" });
|
|
expect(queue.userMessage.value).toBe("Invoice exported");
|
|
});
|
|
|
|
it("handles enqueue success then failed status with retry to completed", async () => {
|
|
vi.useFakeTimers();
|
|
|
|
const requestFn = vi.fn();
|
|
let phase = "initial";
|
|
let initialStatusCalls = 0;
|
|
let retryStatusCalls = 0;
|
|
|
|
requestFn.mockImplementation(async (endpoint, method, payload) => {
|
|
if (endpoint === "/economic/invoice/draft/export" && method === "POST") {
|
|
expect(payload).toEqual({ order_id: 332 });
|
|
return createQueueJobResponse({
|
|
id: 2002,
|
|
status: "QUEUED",
|
|
progress_percent: 0,
|
|
progress_message: "Queued",
|
|
});
|
|
}
|
|
|
|
if (endpoint === "/economic/invoice/draft/export/retry" && method === "POST") {
|
|
expect(payload).toEqual({ job_id: 2002 });
|
|
phase = "retry";
|
|
return createQueueJobResponse({
|
|
id: 2002,
|
|
status: "QUEUED",
|
|
progress_percent: 0,
|
|
progress_message: "Retry queued",
|
|
});
|
|
}
|
|
|
|
if (endpoint === "/economic/invoice/draft/export/status" && method === "GET") {
|
|
expect(payload).toEqual({ job_id: 2002 });
|
|
|
|
if (phase === "initial") {
|
|
initialStatusCalls += 1;
|
|
if (initialStatusCalls === 1) {
|
|
return createQueueStatusResponse({
|
|
id: 2002,
|
|
status: "FAILED",
|
|
progress_percent: 10,
|
|
progress_message: "Validation failed",
|
|
error_message: "no billable order items",
|
|
});
|
|
}
|
|
}
|
|
|
|
retryStatusCalls += 1;
|
|
if (retryStatusCalls === 1) {
|
|
return createQueueStatusResponse({
|
|
id: 2002,
|
|
status: "PROCESSING",
|
|
progress_percent: 70,
|
|
progress_message: "Retry processing",
|
|
});
|
|
}
|
|
|
|
return createQueueStatusResponse({
|
|
id: 2002,
|
|
status: "COMPLETED",
|
|
progress_percent: 100,
|
|
progress_message: "Retry completed",
|
|
result: {
|
|
message: "Draft exported",
|
|
},
|
|
});
|
|
}
|
|
|
|
throw new Error(`Unexpected request ${method} ${endpoint}`);
|
|
});
|
|
|
|
const queue = useEconomicQueueJob({
|
|
enqueueEndpoint: "/economic/invoice/draft/export",
|
|
statusEndpoint: "/economic/invoice/draft/export/status",
|
|
retryEndpoint: "/economic/invoice/draft/export/retry",
|
|
requestFn,
|
|
pollIntervalMs: 200,
|
|
buildEnqueuePayload: (payload) => ({
|
|
order_id: Number.parseInt(String(payload.order_id), 10),
|
|
}),
|
|
});
|
|
|
|
await queue.enqueue({ order_id: 332 });
|
|
|
|
expect(queue.status.value).toBe("FAILED");
|
|
expect(queue.isFailed.value).toBe(true);
|
|
expect(queue.queueFailureMessage.value).toBe("no billable order items");
|
|
expect(queue.canRetry.value).toBe(true);
|
|
|
|
await queue.retry();
|
|
|
|
expect(queue.status.value).toBe("PROCESSING");
|
|
expect(queue.disableSubmit.value).toBe(true);
|
|
|
|
await vi.advanceTimersByTimeAsync(200);
|
|
|
|
expect(queue.status.value).toBe("COMPLETED");
|
|
expect(queue.isCompleted.value).toBe(true);
|
|
expect(queue.result.value).toEqual({ message: "Draft exported" });
|
|
});
|
|
|
|
it("enforces strict boolean send_as_is in collected invoice payload", () => {
|
|
expect(() =>
|
|
buildCollectedInvoiceEconomicPayload({
|
|
id: 123,
|
|
send_as_is: "true",
|
|
})
|
|
).toThrow("send_as_is must be a boolean.");
|
|
|
|
expect(
|
|
buildCollectedInvoiceEconomicPayload({
|
|
id: 123,
|
|
send_as_is: true,
|
|
})
|
|
).toEqual({
|
|
id: 123,
|
|
send_as_is: true,
|
|
});
|
|
});
|
|
|
|
it("parses queue run responses with counts, jobs, and batch limit", () => {
|
|
const run = parseQueueRunResponse({
|
|
data: {
|
|
data: {
|
|
message: "Collected invoice queue batch processed",
|
|
processed: 2,
|
|
completed: 1,
|
|
failed: 1,
|
|
jobs: [4001, "4002", "invalid"],
|
|
limit: 10,
|
|
transfer_type: "COLLECTED_INVOICE_EXPORT",
|
|
},
|
|
},
|
|
});
|
|
|
|
expect(run).toEqual({
|
|
message: "Collected invoice queue batch processed",
|
|
processed: 2,
|
|
completed: 1,
|
|
failed: 1,
|
|
jobs: [4001, 4002],
|
|
limit: 10,
|
|
transfer_type: "COLLECTED_INVOICE_EXPORT",
|
|
});
|
|
});
|
|
|
|
it("stops polling after dispose (unmount/navigation cancellation)", async () => {
|
|
vi.useFakeTimers();
|
|
|
|
const requestFn = vi.fn();
|
|
let statusCallCount = 0;
|
|
|
|
requestFn.mockImplementation(async (endpoint, method) => {
|
|
if (endpoint === "/collected-invoices/economic" && method === "POST") {
|
|
return createQueueJobResponse({
|
|
id: 3003,
|
|
status: "QUEUED",
|
|
progress_percent: 0,
|
|
progress_message: "Queued",
|
|
});
|
|
}
|
|
|
|
if (endpoint === "/collected-invoices/economic/queue/status" && method === "GET") {
|
|
statusCallCount += 1;
|
|
return createQueueStatusResponse({
|
|
id: 3003,
|
|
status: "PROCESSING",
|
|
progress_percent: 50,
|
|
progress_message: "Working",
|
|
});
|
|
}
|
|
|
|
throw new Error(`Unexpected request ${method} ${endpoint}`);
|
|
});
|
|
|
|
const queue = useEconomicQueueJob({
|
|
enqueueEndpoint: "/collected-invoices/economic",
|
|
statusEndpoint: "/collected-invoices/economic/queue/status",
|
|
retryEndpoint: "/collected-invoices/economic/queue/retry",
|
|
requestFn,
|
|
pollIntervalMs: 300,
|
|
buildEnqueuePayload: (payload) => buildCollectedInvoiceEconomicPayload(payload),
|
|
});
|
|
|
|
await queue.enqueue({
|
|
id: 44,
|
|
send_as_is: false,
|
|
});
|
|
|
|
expect(statusCallCount).toBe(1);
|
|
expect(queue.isQueuedOrProcessing.value).toBe(true);
|
|
|
|
queue.dispose();
|
|
|
|
await vi.advanceTimersByTimeAsync(1200);
|
|
|
|
expect(statusCallCount).toBe(1);
|
|
expect(queue.isPolling.value).toBe(false);
|
|
});
|
|
|
|
it("accepts enqueue responses that only include data.job_id", async () => {
|
|
const requestFn = vi.fn();
|
|
|
|
requestFn.mockImplementation(async (endpoint, method, payload) => {
|
|
if (endpoint === "/collected-invoices/economic" && method === "POST") {
|
|
expect(payload).toEqual({
|
|
id: 55,
|
|
send_as_is: false,
|
|
});
|
|
|
|
return {
|
|
data: {
|
|
data: {
|
|
message: "Queued for transfer",
|
|
job_id: 4111,
|
|
job: {
|
|
status: "QUEUED",
|
|
progress_percent: 0,
|
|
progress_message: "Queued",
|
|
},
|
|
},
|
|
},
|
|
};
|
|
}
|
|
|
|
if (endpoint === "/collected-invoices/economic/queue/status" && method === "GET") {
|
|
expect(payload).toEqual({ job_id: 4111 });
|
|
return createQueueStatusResponse({
|
|
id: 4111,
|
|
status: "COMPLETED",
|
|
progress_percent: 100,
|
|
progress_message: "Completed",
|
|
result: {
|
|
message: "Transferred",
|
|
},
|
|
});
|
|
}
|
|
|
|
throw new Error(`Unexpected request ${method} ${endpoint}`);
|
|
});
|
|
|
|
const queue = useEconomicQueueJob({
|
|
enqueueEndpoint: "/collected-invoices/economic",
|
|
statusEndpoint: "/collected-invoices/economic/queue/status",
|
|
retryEndpoint: "/collected-invoices/economic/queue/retry",
|
|
requestFn,
|
|
pollIntervalMs: 250,
|
|
buildEnqueuePayload: (payload) => buildCollectedInvoiceEconomicPayload(payload),
|
|
});
|
|
|
|
await queue.enqueue({
|
|
id: 55,
|
|
send_as_is: false,
|
|
});
|
|
|
|
expect(queue.jobId.value).toBe(4111);
|
|
expect(queue.status.value).toBe("COMPLETED");
|
|
expect(queue.result.value).toEqual({
|
|
message: "Transferred",
|
|
});
|
|
});
|
|
|
|
it("accepts retry responses with job.job_id alias", () => {
|
|
const job = parseQueueJobFromRetryResponse({
|
|
data: {
|
|
data: {
|
|
job: {
|
|
job_id: "8122",
|
|
status: "QUEUED",
|
|
progress_percent: 0,
|
|
progress_message: "Retry queued",
|
|
},
|
|
},
|
|
},
|
|
});
|
|
|
|
expect(job).toEqual({
|
|
id: 8122,
|
|
job_id: "8122",
|
|
status: "QUEUED",
|
|
progress_percent: 0,
|
|
progress_message: "Retry queued",
|
|
error_message: null,
|
|
result: null,
|
|
});
|
|
});
|
|
|
|
it("still throws when enqueue response has no queue job id", () => {
|
|
expect(() =>
|
|
parseQueueJobFromEnqueueResponse({
|
|
data: {
|
|
data: {
|
|
message: "Queued",
|
|
job: {
|
|
status: "QUEUED",
|
|
progress_percent: 0,
|
|
},
|
|
},
|
|
},
|
|
})
|
|
).toThrow("Missing queue job id in enqueue response.");
|
|
});
|
|
|
|
it("parses additive queue list metadata fields from list responses", () => {
|
|
const parsed = parseEconomicTransferQueueListResponse(
|
|
{
|
|
data: {
|
|
data: {
|
|
items: [
|
|
{
|
|
id: 7001,
|
|
status: "PROCESSING",
|
|
progress_percent: 30,
|
|
progress_message: "Exporting",
|
|
attempts: 1,
|
|
max_attempts: 3,
|
|
created_at: "2026-03-10T10:00:00Z",
|
|
},
|
|
],
|
|
count: 1,
|
|
total: 8,
|
|
limit: 1,
|
|
offset: 2,
|
|
has_more: true,
|
|
},
|
|
},
|
|
},
|
|
{
|
|
defaultLimit: 50,
|
|
defaultOffset: 0,
|
|
}
|
|
);
|
|
|
|
expect(parsed.items).toHaveLength(1);
|
|
expect(parsed.items[0]).toMatchObject({
|
|
id: 7001,
|
|
status: "PROCESSING",
|
|
attempts: 1,
|
|
max_attempts: 3,
|
|
});
|
|
expect(parsed.count).toBe(1);
|
|
expect(parsed.total).toBe(8);
|
|
expect(parsed.limit).toBe(1);
|
|
expect(parsed.offset).toBe(2);
|
|
expect(parsed.hasMore).toBe(true);
|
|
});
|
|
|
|
it("keeps additive details_summary payloads intact when parsing queue list responses", () => {
|
|
const parsed = parseEconomicTransferQueueListResponse({
|
|
data: {
|
|
data: {
|
|
items: [
|
|
{
|
|
id: 7401,
|
|
status: "COMPLETED",
|
|
progress_percent: 100,
|
|
details_summary: {
|
|
message: "Transferred",
|
|
target: {
|
|
collected_invoice_id: 501,
|
|
send_as_is: false,
|
|
requested_by: 7,
|
|
},
|
|
customer: {
|
|
customer_number: 43425425,
|
|
name: "Carrier A/S",
|
|
},
|
|
outcome: {
|
|
economic_invoice_draft_id: 91,
|
|
economic_invoice_booked_id: 92,
|
|
external_id: "ext-92",
|
|
total_net_amount: 670,
|
|
order_count: 3,
|
|
},
|
|
raw_available: {
|
|
payload: true,
|
|
result: true,
|
|
},
|
|
},
|
|
},
|
|
],
|
|
},
|
|
},
|
|
});
|
|
|
|
expect(parsed.items).toHaveLength(1);
|
|
expect(parsed.items[0].details_summary).toEqual({
|
|
message: "Transferred",
|
|
target: {
|
|
collected_invoice_id: 501,
|
|
send_as_is: false,
|
|
requested_by: 7,
|
|
},
|
|
customer: {
|
|
customer_number: 43425425,
|
|
name: "Carrier A/S",
|
|
},
|
|
outcome: {
|
|
economic_invoice_draft_id: 91,
|
|
economic_invoice_booked_id: 92,
|
|
external_id: "ext-92",
|
|
total_net_amount: 670,
|
|
order_count: 3,
|
|
},
|
|
raw_available: {
|
|
payload: true,
|
|
result: true,
|
|
},
|
|
});
|
|
});
|
|
|
|
it("derives hasMore deterministically when metadata flag is omitted", () => {
|
|
const parsed = parseEconomicTransferQueueListResponse({
|
|
data: {
|
|
data: {
|
|
items: [
|
|
{ id: 7101, status: "FAILED" },
|
|
{ id: 7102, status: "FAILED" },
|
|
],
|
|
count: 2,
|
|
total: 5,
|
|
limit: 2,
|
|
offset: 2,
|
|
},
|
|
},
|
|
});
|
|
|
|
expect(parsed.hasMore).toBe(true);
|
|
|
|
const tailPage = parseEconomicTransferQueueListResponse({
|
|
data: {
|
|
data: {
|
|
items: [{ id: 7103, status: "COMPLETED" }],
|
|
count: 1,
|
|
total: 5,
|
|
limit: 2,
|
|
offset: 4,
|
|
},
|
|
},
|
|
});
|
|
|
|
expect(tailPage.hasMore).toBe(false);
|
|
});
|
|
|
|
it("guards retry when current queue status is not FAILED", async () => {
|
|
const requestFn = vi.fn();
|
|
|
|
requestFn.mockImplementation(async (endpoint, method) => {
|
|
if (endpoint === "/economic/invoice/export" && method === "POST") {
|
|
return createQueueJobResponse({
|
|
id: 8201,
|
|
status: "QUEUED",
|
|
progress_percent: 0,
|
|
progress_message: "Queued",
|
|
});
|
|
}
|
|
|
|
if (endpoint === "/economic/invoice/export/status" && method === "GET") {
|
|
return createQueueStatusResponse({
|
|
id: 8201,
|
|
status: "COMPLETED",
|
|
progress_percent: 100,
|
|
progress_message: "Completed",
|
|
});
|
|
}
|
|
|
|
throw new Error(`Unexpected request ${method} ${endpoint}`);
|
|
});
|
|
|
|
const queue = useEconomicQueueJob({
|
|
enqueueEndpoint: "/economic/invoice/export",
|
|
statusEndpoint: "/economic/invoice/export/status",
|
|
retryEndpoint: "/economic/invoice/export/retry",
|
|
requestFn,
|
|
pollIntervalMs: 200,
|
|
buildEnqueuePayload: (payload) => payload,
|
|
});
|
|
|
|
await queue.enqueue({ order_id: 11 });
|
|
expect(queue.status.value).toBe("COMPLETED");
|
|
await expect(queue.retry()).rejects.toThrow("Retry is only allowed for failed queue jobs.");
|
|
});
|
|
|
|
it("guards retry when failed job has reached max attempts", async () => {
|
|
const requestFn = vi.fn();
|
|
|
|
requestFn.mockImplementation(async (endpoint, method) => {
|
|
if (endpoint === "/economic/invoice/export" && method === "POST") {
|
|
return createQueueJobResponse({
|
|
id: 8202,
|
|
status: "FAILED",
|
|
progress_percent: 100,
|
|
progress_message: "Failed",
|
|
error_message: "No line items",
|
|
attempts: 3,
|
|
max_attempts: 3,
|
|
});
|
|
}
|
|
|
|
throw new Error(`Unexpected request ${method} ${endpoint}`);
|
|
});
|
|
|
|
const queue = useEconomicQueueJob({
|
|
enqueueEndpoint: "/economic/invoice/export",
|
|
statusEndpoint: "/economic/invoice/export/status",
|
|
retryEndpoint: "/economic/invoice/export/retry",
|
|
requestFn,
|
|
pollIntervalMs: 200,
|
|
buildEnqueuePayload: (payload) => payload,
|
|
});
|
|
|
|
await queue.enqueue({ order_id: 12 });
|
|
expect(queue.status.value).toBe("FAILED");
|
|
expect(queue.canRetry.value).toBe(false);
|
|
await expect(queue.retry()).rejects.toThrow("Retry is not allowed because max attempts were reached.");
|
|
});
|
|
|
|
it("stops polling on transport error and supports a clean polling restart", async () => {
|
|
vi.useFakeTimers();
|
|
|
|
const requestFn = vi.fn();
|
|
let mode = "fail";
|
|
let statusCalls = 0;
|
|
|
|
requestFn.mockImplementation(async (endpoint, method) => {
|
|
if (endpoint === "/collected-invoices/economic" && method === "POST") {
|
|
return createQueueJobResponse({
|
|
id: mode === "fail" ? 9101 : 9102,
|
|
status: "QUEUED",
|
|
progress_percent: 0,
|
|
progress_message: "Queued",
|
|
});
|
|
}
|
|
|
|
if (endpoint === "/collected-invoices/economic/queue/status" && method === "GET") {
|
|
if (mode === "fail") {
|
|
throw {
|
|
response: {
|
|
data: {
|
|
data: {
|
|
message: "gateway timeout",
|
|
},
|
|
},
|
|
},
|
|
};
|
|
}
|
|
|
|
statusCalls += 1;
|
|
if (statusCalls === 1) {
|
|
return createQueueStatusResponse({
|
|
id: 9102,
|
|
status: "PROCESSING",
|
|
progress_percent: 55,
|
|
progress_message: "Working",
|
|
});
|
|
}
|
|
|
|
return createQueueStatusResponse({
|
|
id: 9102,
|
|
status: "COMPLETED",
|
|
progress_percent: 100,
|
|
progress_message: "Completed",
|
|
result: {
|
|
message: "Recovered",
|
|
},
|
|
});
|
|
}
|
|
|
|
throw new Error(`Unexpected request ${method} ${endpoint}`);
|
|
});
|
|
|
|
const queue = useEconomicQueueJob({
|
|
enqueueEndpoint: "/collected-invoices/economic",
|
|
statusEndpoint: "/collected-invoices/economic/queue/status",
|
|
retryEndpoint: "/collected-invoices/economic/queue/retry",
|
|
requestFn,
|
|
pollIntervalMs: 250,
|
|
buildEnqueuePayload: (payload) => buildCollectedInvoiceEconomicPayload(payload),
|
|
});
|
|
|
|
await expect(
|
|
queue.enqueue({
|
|
id: 91,
|
|
send_as_is: false,
|
|
})
|
|
).rejects.toBeDefined();
|
|
|
|
expect(queue.transportErrorMessage.value).toContain("gateway timeout");
|
|
expect(queue.isPolling.value).toBe(false);
|
|
|
|
mode = "success";
|
|
|
|
await queue.enqueue({
|
|
id: 92,
|
|
send_as_is: false,
|
|
});
|
|
|
|
expect(queue.transportErrorMessage.value).toBe("");
|
|
expect(queue.status.value).toBe("PROCESSING");
|
|
|
|
await vi.advanceTimersByTimeAsync(250);
|
|
|
|
expect(queue.status.value).toBe("COMPLETED");
|
|
expect(queue.result.value).toEqual({ message: "Recovered" });
|
|
});
|
|
});
|