Add scheduled invoice delivery

This commit is contained in:
2026-08-17 13:54:45 -04:00
parent 29589c1f32
commit 67ab6b78bd
26 changed files with 2099 additions and 835 deletions
@@ -0,0 +1,88 @@
/// <reference types="bun" />
import { afterEach, describe, expect, mock, test } from "bun:test";
import type { BackgroundJob } from "../../web/src/server/jobs/queue";
import { sendScheduledInvoice } from "../src/send-invoice";
const originalFetch = globalThis.fetch;
const originalAppUrl = process.env.APP_INTERNAL_URL;
const originalSecret = process.env.CRON_SECRET;
function scheduledJob(): BackgroundJob {
const now = new Date("2026-08-17T16:00:00.000Z");
return {
id: "job-1",
type: "invoice.send_scheduled",
payload: {
invoiceId: "invoice-1",
actorUserId: "user-1",
customMessage: "Thanks!",
timeZone: "America/New_York",
},
status: "processing",
idempotencyKey: "invoice.send_scheduled:invoice-1:once",
runAt: now,
attempts: 1,
maxAttempts: 5,
lockedAt: now,
lockedBy: "worker-1",
lastError: null,
completedAt: null,
createdAt: now,
updatedAt: now,
};
}
afterEach(() => {
globalThis.fetch = originalFetch;
if (originalAppUrl === undefined) delete process.env.APP_INTERNAL_URL;
else process.env.APP_INTERNAL_URL = originalAppUrl;
if (originalSecret === undefined) delete process.env.CRON_SECRET;
else process.env.CRON_SECRET = originalSecret;
});
describe("scheduled invoice delivery", () => {
test("calls the internal app endpoint with auth and an idempotency key", async () => {
process.env.APP_INTERNAL_URL = "http://app:3000/";
process.env.CRON_SECRET = "worker-secret";
const fetchMock = mock(
async (_url: string | URL | Request, _request?: RequestInit) =>
Response.json({ success: true, emailId: "email-1" }),
);
globalThis.fetch = fetchMock as unknown as typeof fetch;
await sendScheduledInvoice(scheduledJob());
expect(fetchMock).toHaveBeenCalledTimes(1);
const [url, request] = fetchMock.mock.calls[0]!;
expect(url).toBe("http://app:3000/api/internal/jobs/send-invoice");
expect(request?.headers).toEqual({
Authorization: "Bearer worker-secret",
"Content-Type": "application/json",
});
expect(JSON.parse(String(request?.body))).toMatchObject({
jobId: "job-1",
invoiceId: "invoice-1",
actorUserId: "user-1",
idempotencyKey: "invoice.send_scheduled:invoice-1:once",
timeZone: "America/New_York",
});
});
test("surfaces retryable delivery errors to the worker", async () => {
process.env.APP_INTERNAL_URL = "http://app:3000";
process.env.CRON_SECRET = "worker-secret";
globalThis.fetch = mock(
async (_url: string | URL | Request, _request?: RequestInit) =>
Response.json(
{ error: "Email service is unavailable" },
{ status: 503 },
),
) as unknown as typeof fetch;
await expect(sendScheduledInvoice(scheduledJob())).rejects.toThrow(
"Email service is unavailable",
);
});
});