863 lines
24 KiB
JavaScript
863 lines
24 KiB
JavaScript
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
import { useEconomicQueueJob } from "@/composables/useEconomicQueueJob.js";
|
|
import {
|
|
buildCollectedInvoiceEconomicPayload,
|
|
calculateEconomicTransferQueueProgress,
|
|
dismissEconomicTransferQueueJob,
|
|
dismissTerminalEconomicTransferQueueJobs,
|
|
fetchEconomicTransferQueueMonitor,
|
|
getEconomicQueueMonitorPollIntervalMs,
|
|
isCollectedInvoiceQueueMonitorRoute,
|
|
parseEconomicTransferQueueListResponse,
|
|
parseEconomicTransferQueueMonitorResponse,
|
|
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",
|
|
fallback: false,
|
|
});
|
|
});
|
|
|
|
it("parses monitor responses with status counts and queue progress", () => {
|
|
const monitor = parseEconomicTransferQueueMonitorResponse({
|
|
data: {
|
|
data: {
|
|
jobs: [
|
|
{ id: 501, status: "QUEUED", progress_percent: 0 },
|
|
{ id: 502, status: "PROCESSING", progress_percent: 50 },
|
|
{ id: 503, status: "FAILED", progress_percent: 10 },
|
|
{ id: 504, status: "COMPLETED", progress_percent: 100 },
|
|
],
|
|
counts: {
|
|
queued: 1,
|
|
in_progress: 1,
|
|
failed: 1,
|
|
completed: 1,
|
|
total: 4,
|
|
},
|
|
progress_percent: 63,
|
|
limit: 50,
|
|
},
|
|
},
|
|
});
|
|
|
|
expect(monitor.counts).toEqual({
|
|
queued: 1,
|
|
in_progress: 1,
|
|
failed: 1,
|
|
completed: 1,
|
|
total: 4,
|
|
});
|
|
expect(monitor.progress_percent).toBe(63);
|
|
expect(monitor.jobs.map((job) => job.status)).toEqual(["QUEUED", "PROCESSING", "FAILED", "COMPLETED"]);
|
|
});
|
|
|
|
it("calculates monitor progress when the backend omits aggregate progress", () => {
|
|
expect(
|
|
calculateEconomicTransferQueueProgress([
|
|
{ status: "QUEUED", progress_percent: 0 },
|
|
{ status: "PROCESSING", progress_percent: 50 },
|
|
{ status: "FAILED", progress_percent: 0 },
|
|
{ status: "COMPLETED", progress_percent: 100 },
|
|
])
|
|
).toBe(63);
|
|
|
|
expect(
|
|
parseEconomicTransferQueueMonitorResponse({
|
|
data: {
|
|
data: {
|
|
jobs: [
|
|
{ id: 601, status: "QUEUED", progress_percent: 0 },
|
|
{ id: 602, status: "PROCESSING", progress_percent: 25 },
|
|
],
|
|
},
|
|
},
|
|
}).counts
|
|
).toMatchObject({
|
|
queued: 1,
|
|
in_progress: 1,
|
|
total: 2,
|
|
});
|
|
});
|
|
|
|
it("uses active and idle polling cadences for queue monitor state", () => {
|
|
expect(
|
|
getEconomicQueueMonitorPollIntervalMs({
|
|
counts: {
|
|
queued: 1,
|
|
in_progress: 0,
|
|
},
|
|
})
|
|
).toBe(4000);
|
|
|
|
expect(
|
|
getEconomicQueueMonitorPollIntervalMs({
|
|
counts: {
|
|
queued: 0,
|
|
in_progress: 0,
|
|
failed: 1,
|
|
completed: 1,
|
|
},
|
|
})
|
|
).toBe(30000);
|
|
});
|
|
|
|
it("gates the queue monitor to superuser invoice routes", () => {
|
|
expect(isCollectedInvoiceQueueMonitorRoute("/superuser/invoices")).toBe(true);
|
|
expect(isCollectedInvoiceQueueMonitorRoute("/superuser/invoices/123")).toBe(true);
|
|
expect(isCollectedInvoiceQueueMonitorRoute("/superuser/invoices/distribution/2026/5")).toBe(true);
|
|
expect(isCollectedInvoiceQueueMonitorRoute("/superuser/customers")).toBe(false);
|
|
expect(isCollectedInvoiceQueueMonitorRoute("/admin/1/orders")).toBe(false);
|
|
});
|
|
|
|
it("requests monitor and clear endpoints with the expected payloads", async () => {
|
|
const requestFn = vi.fn();
|
|
requestFn.mockImplementation(async (endpoint, method, payload) => {
|
|
if (endpoint === "/collected-invoices/economic/queue/monitor" && method === "GET") {
|
|
expect(payload).toEqual({ limit: 25 });
|
|
return {
|
|
data: {
|
|
data: {
|
|
jobs: [],
|
|
counts: {
|
|
queued: 0,
|
|
in_progress: 0,
|
|
failed: 0,
|
|
completed: 0,
|
|
total: 0,
|
|
},
|
|
progress_percent: 0,
|
|
limit: 25,
|
|
},
|
|
},
|
|
};
|
|
}
|
|
|
|
if (endpoint === "/collected-invoices/economic/queue/dismiss" && method === "POST") {
|
|
expect(payload).toEqual({ job_id: 7001 });
|
|
return { data: { data: { message: "cleared" } } };
|
|
}
|
|
|
|
if (endpoint === "/collected-invoices/economic/queue/dismiss-terminal" && method === "POST") {
|
|
expect(payload).toEqual({});
|
|
return { data: { data: { dismissed_count: 2 } } };
|
|
}
|
|
|
|
throw new Error(`Unexpected request ${method} ${endpoint}`);
|
|
});
|
|
|
|
const monitorResult = await fetchEconomicTransferQueueMonitor({
|
|
limit: 25,
|
|
requestFn,
|
|
});
|
|
await dismissEconomicTransferQueueJob({
|
|
jobId: 7001,
|
|
requestFn,
|
|
});
|
|
await dismissTerminalEconomicTransferQueueJobs({
|
|
requestFn,
|
|
});
|
|
|
|
expect(monitorResult.monitor.limit).toBe(25);
|
|
expect(requestFn).toHaveBeenCalledTimes(3);
|
|
});
|
|
|
|
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" });
|
|
});
|
|
});
|