payments.ts 2.4 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677
  1. import { expect, type Page } from '@playwright/test'
  2. import { connectionStatus } from './db'
  3. import { settle } from './hydration'
  4. /**
  5. * Online payments, from both ends.
  6. *
  7. * The workshop connects a vendor in Settings → Integrations; the customer pays
  8. * from the shared invoice. Between them sits the stand-in vendor in
  9. * `e2e/payment-sink.ts`, whose state a spec reads to check what the customer
  10. * was actually charged.
  11. */
  12. const sink = process.env.E2E_PAYMENT_SINK ?? 'http://127.0.0.1:8026'
  13. export interface SinkStripeSession {
  14. id: string
  15. payment_status: 'unpaid' | 'paid'
  16. amount_total: number
  17. currency: string
  18. metadata: Record<string, string>
  19. }
  20. export interface SinkPayPalOrder {
  21. id: string
  22. status: string
  23. amount: { currency_code: string; value: string }
  24. custom_id: string
  25. }
  26. export interface SinkState {
  27. stripe: SinkStripeSession[]
  28. paypal: SinkPayPalOrder[]
  29. calls: string[]
  30. }
  31. /** Everything the stand-in vendor was asked for so far. */
  32. export async function paymentSink(): Promise<SinkState> {
  33. const response = await fetch(`${sink}/state`)
  34. if (!response.ok) {
  35. throw new Error(`the payment sink answered ${response.status}. Is e2e/payment-sink.ts running?`)
  36. }
  37. return response.json()
  38. }
  39. export async function clearPaymentSink(): Promise<void> {
  40. await fetch(`${sink}/state`, { method: 'DELETE' })
  41. }
  42. /**
  43. * Types a vendor's keys into its connection page and presses Connect.
  44. *
  45. * The page tests the keys against the vendor before it stores anything as
  46. * live, so the outcome is read back as the connection's status rather than
  47. * from a toast: `active` when the vendor accepted them, anything else when not.
  48. */
  49. export async function connectVendor(
  50. page: Page,
  51. vendor: 'stripe' | 'paypal',
  52. credentials: Record<string, string>
  53. ): Promise<void> {
  54. await page.goto(`/settings/integrations/${vendor}`)
  55. await settle(page)
  56. for (const [key, value] of Object.entries(credentials)) {
  57. const field = page.locator(`input[name="${vendor}-${key}"]`)
  58. await expect(field, `${vendor} asks for ${key}`).toBeVisible()
  59. await field.fill(value)
  60. }
  61. await page.getByRole('button', { name: 'Connect', exact: true }).click()
  62. }
  63. /** Waits for the connection to settle into the status a spec expects. */
  64. export async function expectConnection(vendor: string, status: string): Promise<void> {
  65. await expect
  66. .poll(() => connectionStatus(vendor), { timeout: 30_000, message: `${vendor} is ${status}` })
  67. .toBe(status)
  68. }