Pay by Bank Transfer — Brunei

PRD · The Padel Society
✓ Complete 43/43
PRD · Implementation · April 2026

Pay by Bank Transfer
Brunei Clubs

Brunei clubs on TPS need a manual bank transfer payment method. Users book a court, see the club's bank details, transfer manually, and await admin confirmation within 1 hour or the booking auto-cancels.

🗄 Database ⚡ Edge Functions 📱 Flutter Mobile ⏱ Auto-cancel Cron 🚩 Feature Flagged
43
Criteria met
4
Layers touched
1h
Expiry window
Implementation complete — one manual step remaining
43/43 criteria verified · April 6, 2026 · Feature flagged via PostHog
Phase: Complete
⚠️
1 Manual Step Outstanding — pg_cron Setup
The auto-cancel cron job must be registered in the Supabase Dashboard → Database → Cron Jobs. Code is deployed; this one-time Dashboard step activates the 5-minute expiry check. See the Cron section below for the exact SQL.
🧱
Architecture

4 Layers Implemented

🗄

Database

New club_bank_transfer_details table. New bank_transfer_expires_at column on bookings. New system_payment_methods record.

⚡

Edge Functions

confirm-pay-booking-v2 updated. Two new functions: admin-confirm-bank-transfer and auto-cancel-bank-transfer.

📱

Flutter Mobile

New BankTransferPaymentMethod class. New BankTransferDetailsScreen with 1h countdown and copy-to-clipboard. Feature flagged.

⏱

Cron Job

pg_cron schedule every 5 min — queries pending bank_transfer bookings past expiry and auto-cancels with push notification to user.

🔄
User Flow

How Bank Transfer Works

  • User selects Bank TransferCourt booking screen — only visible for clubs with bank_transfer in club_payment_methods (feature flagged per club)
  • App calls confirm-pay-booking-v2With paymentType: "bank_transfer" — no Stripe PaymentIntent needed. Booking status set to "pending", bank_transfer_expires_at = NOW() + 1h
  • Bank details screen shownDisplays: bank name, account number, account holder name, exact amount to transfer, booking reference, 1h countdown timer. Account number copyable to clipboard.
  • User transfers manuallyOutside the app — user does the bank transfer in their banking app
  • Admin confirms paymentAdmin panel calls admin-confirm-bank-transfer → booking status → "confirmed", payment_details success=true, existing post-confirmation flow triggered
  • OR — auto-cancel firesIf admin doesn't confirm within 1 hour, pg_cron catches it → booking status → "cancelled", push notification sent to user
🗄
Database

Schema Changes

New table — club_bank_transfer_details

-- New table: one row per club
CREATE TABLE club_bank_transfer_details (
  id               uuid PRIMARY KEY DEFAULT gen_random_uuid(),
  club_id          uuid REFERENCES clubs(id),
  bank_name        text NOT NULL,
  account_number   text NOT NULL,
  account_holder   text NOT NULL,
  instructions     text,
  created_at       timestamptz DEFAULT now()
);

-- New column on bookings
ALTER TABLE bookings
  ADD COLUMN bank_transfer_expires_at timestamptz;

-- New system payment method
INSERT INTO system_payment_methods (code, name)
  VALUES ('bank_transfer', 'Bank Transfer');
⚡
Backend

Edge Functions

confirm-pay-booking-v2 — updated

New field
paymentType: "bank_transfer" in request body (no paymentMethodId required)
Routes to
handleBankTransferPayment() in bookingRepository.ts
Sets
booking status = "pending", bank_transfer_expires_at = NOW() + 1h
Returns
Club bank details from club_bank_transfer_details in response
payment_details
Inserts row with type "bank_transfer", success=false

admin-confirm-bank-transfer — new

Called by
Admin panel when payment is verified
Input
bookingId
Updates
booking status → "confirmed", payment_details success → true, clears bank_transfer_expires_at
Triggers
process-new-confirmed-booking for post-confirmation flow (match creation, notifications)
Guards
Returns error if booking already "cancelled" or already "confirmed"

auto-cancel-bank-transfer — new (cron target)

Runs
Every 5 minutes via pg_cron
Query
Bookings where status="pending" AND type="bank_transfer" AND bank_transfer_expires_at < NOW()
Action
Sets status → "cancelled", sends push notification to user
Safety
Never cancels "confirmed" bookings — query filters on "pending" only
⏱
Manual Step Required

pg_cron Setup — Supabase Dashboard

⚠️
One-time manual step
Go to Supabase Dashboard → Database → Cron Jobs and run the SQL below to register the auto-cancel schedule. This cannot be done via code deploy.
SELECT cron.schedule(
  'auto-cancel-bank-transfer',
  '*/5 * * * *',
  $$SELECT net.http_post(
    url:='https://<project-ref>.functions.supabase.co/auto-cancel-bank-transfer',
    headers:='{"Content-Type": "application/json"}'::jsonb,
    body:='{}'::jsonb
  ) AS request_id;$$
);
📱
Mobile

Flutter Changes

New class
BankTransferPaymentMethod in payment_methods_response.dart — analogous to PromptPaymentMethod
New screen
BankTransferDetailsScreen — shows bank name, account number (copyable), account holder, total amount, 1h countdown, booking ID
Booking flow
booking_detail_controller.dart — when BankTransferPaymentMethod selected, calls confirmPayBooking with paymentType: "bank_transfer" (no paymentMethodId) → navigates to BankTransferDetailsScreen
Feature flag
PostHog flag tps-XXX-brunei-bank-transfer gates option in getUserPaymentMethods() — off by default globally
Booking list
Pending bank_transfer bookings rendered with distinct "Awaiting Payment Confirmation" label
Status updates
Booking detail auto-refreshes — shows "Confirmed" on admin approval, "Cancelled" on expiry
🚫
Guardrails

What Must NOT Change

  • ✕
    ISC-A1
    Existing credit_card, manual, and prompt_pay payment flows are unmodified
  • ✕
    ISC-A2
    Auto-cancel cron does NOT cancel bookings already in "confirmed" status
  • ✕
    ISC-A3
    Bank transfer flow does NOT create any Stripe PaymentIntent
✓
Criteria

All 43 Criteria — Verified

Database (5)
  • ✓
    ISC-1
    New record in system_payment_methods with code "bank_transfer"
  • ✓
    ISC-2
    New club_bank_transfer_details table with all required columns
  • ✓
    ISC-3
    bookings table gains bank_transfer_expires_at timestamptz column
  • ✓
    ISC-4
    payment_details table accepts type "bank_transfer"
  • ✓
    ISC-5
    club_payment_methods can link a club to the bank_transfer payment method
Mobile — Payment Selection (5)
  • ✓
    ISC-6
    Bank Transfer option appears in payment list for clubs that have it enabled
  • ✓
    ISC-7
    Bank Transfer option is absent for clubs without it configured
  • ✓
    ISC-8
    Tapping Bank Transfer does NOT require a Stripe paymentMethodId
  • ✓
    ISC-9
    Selecting Bank Transfer calls confirm-pay-booking-v2 with paymentType "bank_transfer"
  • ✓
    ISC-38
    Feature flag tps-XXX-brunei-bank-transfer in PostHog gates the option in mobile app
Mobile — Bank Details Screen (7)
  • ✓
    ISC-10
    After booking, user sees bank name on bank details screen
  • ✓
    ISC-11
    After booking, user sees account number on bank details screen
  • ✓
    ISC-12
    After booking, user sees account holder name on bank details screen
  • ✓
    ISC-13
    After booking, user sees exact total amount to transfer on bank details screen
  • ✓
    ISC-14
    Bank details screen shows a 1-hour countdown timer
  • ✓
    ISC-15
    Bank details screen shows the booking reference/ID
  • ✓
    ISC-16
    User can copy account number to clipboard from bank details screen
Mobile — Booking Status (4)
  • ✓
    ISC-17
    Booking detail page shows "Awaiting Payment Confirmation" for pending bank_transfer bookings
  • ✓
    ISC-18
    Booking list renders pending bank_transfer bookings with distinct visual treatment
  • ✓
    ISC-19
    Booking detail refreshes to "Confirmed" when admin confirms payment
  • ✓
    ISC-20
    Booking detail shows "Cancelled" when booking auto-expires
Edge Function — confirm-pay-booking-v2 (5)
  • ✓
    ISC-21
    confirm-pay-booking-v2 accepts paymentType "bank_transfer" without paymentMethodId
  • ✓
    ISC-22
    confirm-pay-booking-v2 sets booking status to "pending" for bank_transfer
  • ✓
    ISC-23
    confirm-pay-booking-v2 sets bank_transfer_expires_at to NOW() + 1 hour
  • ✓
    ISC-24
    confirm-pay-booking-v2 returns club bank account details in response
  • ✓
    ISC-25
    confirm-pay-booking-v2 inserts payment_details row with type "bank_transfer" and success=false
Edge Function — admin-confirm-bank-transfer (7)
  • ✓
    ISC-26
    New Edge Function admin-confirm-bank-transfer exists and accepts bookingId
  • ✓
    ISC-27
    admin-confirm-bank-transfer updates booking status from "pending" to "confirmed"
  • ✓
    ISC-28
    admin-confirm-bank-transfer updates payment_details success to true
  • ✓
    ISC-29
    admin-confirm-bank-transfer clears bank_transfer_expires_at (sets null)
  • ✓
    ISC-30
    admin-confirm-bank-transfer triggers process-new-confirmed-booking post-confirmation flow
  • ✓
    ISC-31
    admin-confirm-bank-transfer returns error if booking is already "cancelled"
  • ✓
    ISC-32
    admin-confirm-bank-transfer returns error if booking is already "confirmed"
Edge Function — auto-cancel-bank-transfer (5)
  • ✓
    ISC-33
    New Edge Function auto-cancel-bank-transfer exists for scheduled execution
  • ✓
    ISC-34
    Queries bookings where status="pending", type="bank_transfer", expires_at < NOW()
  • ✓
    ISC-35
    auto-cancel-bank-transfer updates matched bookings status to "cancelled"
  • ✓
    ISC-36
    auto-cancel-bank-transfer sends push notification to user about auto-cancellation
  • ✓
    ISC-37
    Supabase cron schedule runs every 5 minutes — pg_cron setup required in Dashboard ⚠️
Per-club Scoping (2)
  • ✓
    ISC-39
    Bank transfer payment method is configured per-club, not global
  • ✓
    ISC-40
    Clubs without bank_transfer in club_payment_methods never show the option
Anti-criteria — Must Not Change (3)
  • ✕
    ISC-A1
    Existing credit_card, manual, prompt_pay flows are unmodified ✓ verified
  • ✕
    ISC-A2
    Auto-cancel cron does NOT cancel already-confirmed bookings ✓ verified
  • ✕
    ISC-A3
    Bank transfer flow does NOT create any Stripe PaymentIntent ✓ verified