Files
hope-events/e2e/payfast.spec.ts
T
joshuaandClaude Opus 5.5 f2b51dbd88 PayFast provider, Pay online page, amount checks on payments
- PayFast (custom integration): signed form (PHP-style encoding, checked
  against a real sandbox), ITN checks (signature over all posted fields,
  PayFast host, VALID from /eng/query/validate, amount vs checkout).
- ProviderCheckout records every checkout (amount, metadata, request);
  webhooks match payments to it and hold a different amount for review
  (ProviderTransaction.note).
- /pay/<id>: payers choose a provider when several are on, and the
  browser posts PayFast's form from there, so payment links work for
  every provider.
- Site settings → Payments: PayFast card (merchant ID/key, encrypted
  passphrase, sandbox/live) while PayFast is enabled.
- Payer-facing wording no longer names Yoco; legal pages name both.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 14:25:49 +02:00

52 lines
3.0 KiB
TypeScript

import { test, expect, seed, loginAs, api } from "./fixtures";
// Pays into a real PayFast sandbox account, so it needs one:
// E2E_PAYFAST_MERCHANT_ID, E2E_PAYFAST_MERCHANT_KEY, E2E_PAYFAST_PASSPHRASE
// (from the Sandbox dashboard). Skipped when they aren't set, e.g. in CI.
const PF = {
merchantId: process.env.E2E_PAYFAST_MERCHANT_ID || "",
merchantKey: process.env.E2E_PAYFAST_MERCHANT_KEY || "",
passphrase: process.env.E2E_PAYFAST_PASSPHRASE || "",
};
type EventWithOptions = { eventOptions: { id: string; isMainTicket: boolean }[] };
type Registration = { id: string };
type Checkout = { checkoutId: string; redirectUrl: string; provider: string | null };
test.describe("PayFast", () => {
test.skip(!PF.merchantId, "Set E2E_PAYFAST_* to test against a PayFast sandbox account");
test("choose PayFast on the pay page and land on PayFast's payment page", async ({ page }) => {
await api(page, "admin", "PUT", "/api/payfast/config", { ...PF, mode: "sandbox" });
try {
await api(page, "admin", "PUT", "/api/payments/providers/settings", { providers: [{ id: "yoco", enabled: true }, { id: "payfast", enabled: true }] });
// A booking for the paid conference, then an online checkout for it
const event = await api<EventWithOptions>(page, "staff", "GET", `/api/events/${seed().events.paid}`);
const option = event.eventOptions.find((o) => o.isMainTicket)!;
const reg = await api<Registration>(page, "staff", "POST", "/api/registrations", { eventId: seed().events.paid, options: [{ eventOptionId: option.id, quantity: 1 }] });
const origin = new URL(page.url() === "about:blank" ? "http://localhost:3100" : page.url()).origin;
const checkout = await api<Checkout>(page, "staff", "POST", "/api/payments/checkout", {
registrationId: reg.id, successUrl: `${origin}/payment/success`, cancelUrl: `${origin}/payment/cancel`, failureUrl: `${origin}/payment/failure`,
});
// Yoco and PayFast are both on, so the payer chooses
expect(checkout.provider).toBeNull();
expect(checkout.redirectUrl).toContain(`/pay/${checkout.checkoutId}`);
await loginAs(page, "staff");
await page.goto(new URL(checkout.redirectUrl).pathname);
await expect(page.getByText("R150.00")).toBeVisible();
await expect(page.getByRole("button", { name: "Pay with Yoco" })).toBeVisible();
await page.getByRole("button", { name: "Pay with PayFast" }).click();
// The browser posts the signed form to the sandbox, which shows its payment page
await page.waitForURL(/sandbox\.payfast\.co\.za/, { timeout: 30_000 });
await expect(page.locator("body")).not.toContainText(/signature/i);
await expect(page.locator("body")).toContainText(/150[.,]00/);
} finally {
await api(page, "admin", "PUT", "/api/payments/providers/settings", { providers: [{ id: "yoco", enabled: true }, { id: "payfast", enabled: false }] });
await api(page, "admin", "PUT", "/api/payfast/config", { merchantId: "", merchantKey: "", passphrase: "" });
}
});
});