- src/payments/status.js: open checkouts, checkCheckouts and the payment-status job (every 15 min, checkouts 10 min - 3 days old). Found payments are settled exactly as their webhook would be (settlePayment, extracted from the webhook handler: amount check, record, emails). - PayFast: signed API client (alphabetical signature, testing=true in sandbox), findPayments from the transaction history CSV matched on M Payment ID, and testConnection (signed /ping). Verified against a real sandbox account. - Yoco documents no checkout lookup, so its checkouts are "can't check". - Payments -> Reconcile: "Waiting for payment" with Check status / Check all; PayFast card: Test connection. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
61 lines
3.6 KiB
TypeScript
61 lines
3.6 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/);
|
|
|
|
// Not paid at PayFast: the status check asks PayFast's API (signed) and says so
|
|
await loginAs(page, "supervisor");
|
|
await page.goto("/dashboard/supervisor/payments");
|
|
await page.getByText("Reconcile", { exact: true }).click();
|
|
const row = page.getByRole("row").filter({ hasText: "PayFast" }).filter({ hasText: "R 150.00" }).first();
|
|
await row.getByRole("button", { name: "Check status" }).click();
|
|
await expect(row.getByText("Not paid (yet)")).toBeVisible({ timeout: 30_000 });
|
|
await expect(page.getByText("No new payments found.")).toBeVisible();
|
|
} 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: "" });
|
|
}
|
|
});
|
|
});
|