Skip to content

fix(payment): gross up the gateway fee + settle every paid cart

Maryam Ghorbani requested to merge feat/payment-credit-cash into beta

Two money rules the in-chat checkout owed the platform. Together they are why a booking could take the user's money and never issue a ticket — the symptom in docs/wallet_partial_diagnosis.md, on the partial-wallet AND the plain-gateway path alike.

  1. GATEWAY FEE GROSS-UP. On a successful payment the platform credits the wallet with exactly the amount /payment/pay was called with, then withdraws commissionPercentage% of it as the processor fee (payment gateway.service.ts::depositToWallet, mirrored in ledger.service.ts for an owner of type USER). We charged the bare cart amount, so the wallet always landed a few percent SHORT of the cart and the checkout that settles the cart from that wallet could never succeed. The site has always grossed this up (CalculationPaymentFees); we now do the same, ceil-to-0.1 included — rounding down would leave the wallet a cent short, which is the whole failure. gateway.service.ts::validatePaymentRequest corroborates: it inflates the gateway's min/max by the same factor. -> booking_logic.gross_up, applied per gateway in prepare_checkout AFTER the currency conversion. GatewayOptionDTO gains net_amount + commission_percentage.

  2. SETTLE EVERY PAID CART. A gateway payment does not create the order; the cart is only turned into one by GET /booking/book/cart/{id}/checkout/. The site calls it on EVERY return with status=succeeded (purchase-receipt/[cartId]/[productType].tsx) — full gateway, partial, or wallet-only. We gated it on a fresh wallet read COVERING the cart, which (because of 1) was false on exactly the carts that needed settling. There is no platform auto-settle to double up with; re-entering a settled cart is a no-op booking side (the worker locks FOR UPDATE and only reprocesses REGISTERED/payment-failure). -> _settle_from_wallet_after_payment becomes _settle_cart_after_payment, no coverage precondition. It also read the returned BookingResultDTO with .get(), which raised AttributeError on every call and hid the outcome; a decline now logs at ERROR.

Also, so the credit/cash handling is legible and every scenario has a way forward:

  • CASH-FIRST SPLIT. wallet_covers reports from_cash/from_credit — the order the platform spends them in, and the same cascade the site prints on its receipt (fundingApplied). The old single-comparison paid_from labelled a mostly-cash payment 'credit'. The confirm gate and the partial split line now name both (Telegram renders the extra line only when the deduction is genuinely mixed).
  • WALLET DECLINE NO LONGER DEAD-ENDS. A declined reserve saga left the user on payment_failed with the booking parked at wallet_confirm. prepare_checkout deliberately keeps the real gateways for this case — re-show them with the reason above the menu.

Contract: confirm_wallet/select_gateway gain optional from_cash/from_credit; a bridge that ignores them renders exactly as before. CONTRACT.md + CLAUDE.md updated, and the diagnosis doc now carries the resolution (Q1 answered: yes, the bot must always call checkout; Q2 — the dropped ref — is still open with the platform team but no longer load-bearing).

Tests: 19 new (test_gateway_fee_and_funding.py — gross-up parity with the site formula, "platform is whole after the fee", the cash-first split incl. the reseller no-cash-wallet shape; test_wallet_settle.py rewritten around the unconditional settle; two booking-journey tests for the split on the gate and the decline fallback). Run in a throwaway venv: 307 passed, with the same 9 failures / 6 errors as before the change — all missing optional deps in that venv (langchain_openai, tiktoken, psycopg), none touched by this branch. The suite was NOT run whole in its real uv/py3.14 environment.

Co-Authored-By: Claude Opus 5 noreply@anthropic.com Claude-Session: https://claude.ai/code/session_01NUV5mLHt2gMksbK9QKyKji

Merge request reports

Loading