/** * Who is allowed to touch a payment. Cheap to run (no booking chain), and the * failures here are the expensive kind: a tenant reading another tenant's * invoice, or an unauthenticated caller marking one paid. */ import { afterAll, describe, expect, it } from "vitest"; import request from "supertest"; import { API, PAYMENT_API, api, closeDb, customerA, customerB, db, login, payment, } from "./client"; describe("payment authorization boundaries", () => { afterAll(closeDb); it("hides one tenant's invoice from the other", async () => { const rows = await db<{ id: string; company_id: string }>( `SELECT i.id, i.company_id FROM freight.invoices i JOIN freight.companies c ON c.id = i.company_id WHERE c.tin = '0102030405' AND i.deleted_at IS NULL ORDER BY i.created_at DESC LIMIT 1`, ); if (!rows[0]) return; // nothing billed yet in this run — payment files cover it const res = await api(customerB, "get", `/api/billing/my-invoices/${rows[0].id}`); expect([403, 404]).toContain(res.status); }); it("refuses to let one tenant pay the other's invoice", async () => { const rows = await db<{ id: string }>( `SELECT i.id FROM freight.invoices i JOIN freight.companies c ON c.id = i.company_id WHERE c.tin = '0102030405' AND i.status <> 'PAID' AND i.deleted_at IS NULL ORDER BY i.created_at DESC LIMIT 1`, ); if (!rows[0]) return; const res = await api(customerB, "post", `/api/billing/my-invoices/${rows[0].id}/pay`, { method: "CBE_BIRR", platform: "web", }); expect(res.status).toBeGreaterThanOrEqual(400); }); it("keeps a portal customer out of backoffice payment operations", async () => { const res = await api(customerA, "get", "/api/billing/invoices"); expect(res.status).toBeGreaterThanOrEqual(400); }); it("rejects a portal account on the backoffice login audience", async () => { const res = await login(customerA, "12345678", "backoffice"); expect(res.status).toBeGreaterThanOrEqual(400); }); it("requires the service token on freight's mark-paid callback", async () => { const body = { version: 1, eventId: "authz-probe", eventType: "payment.succeeded", occurredAt: new Date().toISOString(), service: "FREIGHT", intentId: "00000000-0000-0000-0000-000000000000", referenceType: "SHIPMENT", referenceId: "00000000-0000-0000-0000-000000000000", provider: "CBE_BIRR", amountMinor: 1, currency: "ETB", }; const res = await request(API).post("/api/internal/payments/mark-paid").send(body); expect([401, 403]).toContain(res.status); }); it("requires the service token on the payment API's internal surface", async () => { const res = await request(PAYMENT_API).get("/payments/intents?service=FREIGHT"); expect([400, 401, 403]).toContain(res.status); // …and accepts it when present (400 = bad query, not an auth failure). const withToken = await payment("get", "/payments/intents?service=FREIGHT"); expect([401, 403]).not.toContain(withToken.status); }); it("leaves the provider webhook surface public — trust is the signature", async () => { // A garbage payload must be acked, not 401'd: providers do not authenticate. const res = await request(PAYMENT_API).post("/webhooks/cbe-birr").send({ nonsense: true }); expect(res.status).toBe(200); }); });