Payment — Common cases
Method missing at checkout
Check the master toggle, BU/provider setting, category availability, site scope, and payment option rule. If the method is hidden correctly by config, do not change the checkout flow.
Real-world example: A parent at site A does not see Direct Debit at checkout. → Determine whether the method is hidden by configuration or by a bug. → The provider is not enabled for site A's BU → this is correct config-driven hiding; do not change the checkout flow.
PayNow succeeds but the UI does not update
Read the provider result/webhook, batch/payment status, and checkout success method resolution. The webhook can arrive after the redirect; the UI must tolerate a short pending state.
Real-world example: A customer finishes paying by card and is returned to a "processing" page for a few seconds before success is shown. → The UI must not conclude failure too early. → The webhook arrives after the redirect → keep a short pending state, then update when the webhook lands.
Direct Debit creates a customer in the wrong BU
AccountPaymentCustomer must be unique/active by (AccountId, BusinessUnitId, PaymentProvider). If the BU filter is missing, it is a boundary/security error, not just a query bug.
Real-world example: An account is active in 2 BUs; setting Direct Debit in BU2 points back to BU1's customer. → Avoid payment leakage between the two units. → The query is missing the BU filter → treat it as a boundary error and add
BusinessUnitIdto the unique condition.
Deposit double submit
Check the order lock, provider callback idempotency, and batch state. Deposit must not create multiple transaction slots for the same order while the lock is active.
Real-world example: A customer clicks the deposit button twice because the network is slow. → Avoid charging deposit twice for one order. → Order lock + callback idempotency block the second slot while the lock is active.
Checkout success message shows the wrong method
Cross-check payment-success-message-by-method, the checkout confirmation type guard, and method resolution. The message should reflect the actual method, not be guessed from the route.
Real-world example: A customer pays by Direct Debit but the success screen shows PayNow copy. → The message must match the actual method so the customer is not misled. → Resolve the message from the transaction's real method, not guessed from the checkout route.
Zero-amount checkout does not open a provider
Total = 0 → skip the provider regardless of the chosen method; Proceed confirms immediately, Submit keeps pending by eligibility.
Real-world example: After a discount a booking still has total = 0, and the customer picks Pay Now. → Do not redirect to Windcave for a zero-amount charge. → The
Proceedbutton appears; once clicked the booking confirms immediately and returns to the confirmed success surface.
Zero-amount Pay Later-only stays pending
When total = 0 and Pay Later is the only method → Submit button; the booking stays Submitted/pending and shows the pending message; no redirect to booking-confirmed.
Real-world example: The category only allows Pay Later and the booking has total = 0. → Cannot confirm immediately because there is no collection action yet. → The
Submitbutton keeps the booking Submitted; admin/staff Confirm later.
Background finalization popup after paying
total payable > 0 and the provider returns success → a popup says the system is finalizing the booking in the background; no OK click needed; it closes by itself when done.
Real-world example: A customer finishes paying the deposit and is redirected back to the portal, but the booking is not confirmed yet. → Prevent the customer from closing the tab and interrupting finalization. → A popup "processing, don't close the browser" appears and disappears by itself when finalization completes.
PayLater display name per BU
Admin sets PayLaterDisplayName on the "Other" tab → checkout shows that label for every category; null/empty → fallback "Pay Later"; if the category forbids Pay Later the option is not shown even if the label exists.
Real-world example: A BU sets "Pay by Invoice". → Customers see "Pay by Invoice" instead of "Pay Later". → The correct label is shown; but if the category is
None, the option is still not shown.
Mixed FT/PT/Casual booking considered for Pay Later
Precedence FULL_TIME > PART_TIME > CASUAL: ≥1 FT session → Full Time; no FT but has PT → Part Time; only Casual → does not satisfy FT/PT.
Real-world example: A booking has 1 FT session + 2 Casual sessions, and the category sets an FT rule. → Determine whether the booking satisfies FT. → It counts as Full Time by precedence → Pay Later is available.
Master toggle on but category forbids
Provider enabled at the BU ≠ booking eligibility at the category. A method only shows when both pass; changing one layer does not override the other.
Real-world example: Pay Now is enabled with Stripe at the BU but category B is
Disabled. → Do not mix up the two configs. → The Pay Now section stays enabled in settings, but customers checking out category B do not see Pay Now.
Change payment method on billing
Booking Manager changes a billing's PaymentOptionId via ChangeBillingPaymentMethodModal (Direct Debit = 4 / Other = 1); the button only shows when the BU has a DD provider; a backend error → keep the modal open and show a message.
Real-world example: Staff wants to switch a billing from "Other" to Direct Debit for a booking. → Avoid creating a new billing. → Open the modal, pick Direct Debit, save successfully; if the BU has not enabled DD the button does not show.
Success message by method and fallback
Batch type 201 → payNowSuccessMessage; 204 → payNowDepositSuccessMessage, falling back to payNowSuccessMessage if the deposit message is empty; DD/Pay Later confirmed → directDebitSuccessMessage/payLaterMessage; empty message → default, do not show deleted old content.
Real-world example: A customer pays the deposit but the BU has not written a deposit message. → Do not leave the success page blank. → Show the current
payNowSuccessMessageinstead of an empty page or deleted old content.