Timed jobs move from in-process timers to a job registry run by pg-boss: once per schedule across processes, retries, run history, cron schedules in the church timezone, and an admin Background jobs page with Run now. Playwright end-to-end tests run against their own seeded database. A Gitea Actions workflow runs backend tests, frontend checks and the e2e tests, and deploys v* tags with scripts/deploy.sh once secrets are set. Also fixes the at-the-door check-in count for multi-day tickets. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
54 lines
2.9 KiB
TypeScript
54 lines
2.9 KiB
TypeScript
import { test, expect, seed, loginAs, api } from "./fixtures";
|
|
|
|
type Registration = { id: string; status: string; eventId: string };
|
|
type Ticket = { id: string; qrCode: string; eventId: string };
|
|
|
|
type CashByUser = { rows: { userId: string | null; cash: { total: number; count: number } }[] };
|
|
|
|
test.describe("registration → payment → ticket → check-in → cashup", () => {
|
|
test("a free event: register and get a ticket straight away", async ({ page }) => {
|
|
await loginAs(page, "user");
|
|
await page.goto(`/register/${seed().events.free}`);
|
|
await expect(page.getByText("E2E Free Evening").first()).toBeVisible();
|
|
await page.getByRole("button", { name: "+" }).first().click();
|
|
await page.getByRole("button", { name: "Register", exact: true }).click();
|
|
await page.waitForURL(/\/registration\/success/);
|
|
|
|
const tickets = await api<Ticket[]>(page, "user", "GET", "/api/tickets/mytickets");
|
|
expect(tickets.some((t) => t.eventId === seed().events.free)).toBe(true);
|
|
});
|
|
|
|
test("a paid event: pending until paid, then a ticket that checks in once", async ({ page }) => {
|
|
await loginAs(page, "user");
|
|
await page.goto(`/register/${seed().events.paid}`);
|
|
await page.getByRole("button", { name: "+" }).first().click();
|
|
await expect(page.getByText("Total: R150.00")).toBeVisible();
|
|
await page.getByRole("button", { name: "Register", exact: true }).click();
|
|
await page.waitForURL(/\/registration\/success/);
|
|
|
|
const regs = await api<Registration[]>(page, "user", "GET", "/api/registrations/myregistrations");
|
|
const reg = regs.find((r) => r.eventId === seed().events.paid);
|
|
expect(reg?.status).not.toBe("paid");
|
|
expect((await api<Ticket[]>(page, "user", "GET", "/api/tickets/mytickets")).some((t) => t.eventId === seed().events.paid)).toBe(false);
|
|
|
|
// A supervisor records the cash payment at the desk.
|
|
await api(page, "supervisor", "POST", "/api/payments", { amount: 150, method: "cash", registrationId: reg!.id, eventId: seed().events.paid, userId: seed().users.user.id });
|
|
const ticket = (await api<Ticket[]>(page, "user", "GET", "/api/tickets/mytickets")).find((t) => t.eventId === seed().events.paid);
|
|
expect(ticket).toBeTruthy();
|
|
|
|
// My Events shows it.
|
|
await page.goto("/dashboard/user");
|
|
await expect(page.getByText("E2E Paid Conference").first()).toBeVisible();
|
|
|
|
// Staff check it in; a second scan is refused.
|
|
await api(page, "staff", "POST", `/api/tickets/scan/${ticket!.qrCode}`, { qty: 1 });
|
|
await expect(api(page, "staff", "POST", `/api/tickets/scan/${ticket!.qrCode}`, { qty: 1 })).rejects.toThrow(/403.*fully used/);
|
|
|
|
// Cashup: the R150 cash is on the supervisor who took it.
|
|
const cash = await api<CashByUser>(page, "supervisor", "GET", `/api/cashups/event/${seed().events.paid}/cash-by-user`);
|
|
const mine = cash.rows.find((r) => r.userId === seed().users.supervisor.id);
|
|
expect(mine?.cash.total).toBe(150);
|
|
expect(mine?.cash.count).toBe(1);
|
|
});
|
|
});
|