mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-27 00:52:50 +00:00
93 lines
3.4 KiB
TypeScript
93 lines
3.4 KiB
TypeScript
/**
|
|
* 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);
|
|
});
|
|
});
|