Services

Backend Engineering, Integration

Industry

E-Commerce SaaS

Year

2022-2024

Three payment gateways. Three architectures. One reliable checkout.

A SaaS platform selling digital download codes needed to accept credit cards, PayPal, and European bank transfers. Each gateway had a fundamentally different integration pattern: Braintree processed cards synchronously in a single API call, PayPal required a redirect-and-return flow with a separate execution step, and Sofort operated entirely through asynchronous XML webhooks. The original implementation treated each as a standalone code path, with duplicated finalization logic and no protection against price manipulation between approval and execution.

01.
THE CHALLENGE

Three Gateways, Three Failure Modes

Each payment provider imposed a different contract. Braintree returned a result object immediately - success or failure in one round-trip. PayPal required the user to leave the site, approve on paypal.com, then return, at which point the system had to make a second API call to execute the charge. Sofort was the most complex: the system posted XML to create a transaction, redirected the user to their bank's website, then waited for webhook notifications that arrived minutes to hours later. A single order could end up in a limbo state if the PayPal redirect timed out, if Sofort's webhook never arrived, or if the user modified their code package between initiating and completing payment. The finalization logic - generating codes, sending invoices, calculating referral commissions - was copy-pasted across all three paths, each with slightly different error handling.

PayPal redirects you offsite. Sofort sends a webhook hours later. Both need to finalize the same order. The code was duplicated three times.

02.
THE SOLUTION

Unified Finalization with Per-Gateway Adapters

I restructured the payment layer around a clear separation: gateway-specific logic handles the mechanics of each provider, but every successful charge converges into a shared finalization path. Credit card payments call Braintree's API and finalize inline. PayPal payments re-validate the price on return before executing, catching any tampering during the redirect window. Sofort payments lock the code package as non-editable when initiated and rely on webhook notifications to trigger finalization. The referral commission calculation, code generation, and invoice dispatch run identically regardless of which gateway completed the charge.

The credit card flow - synchronous, single round-trip:

Python
@standard_ajax
def create_braintree_payment(request, code_package):
    nonce = request.POST.get('payment_method_nonce')
    amount = str(code_package.calculated_price)

    result = braintree.Transaction.sale({
        'amount': amount,
        'payment_method_nonce': nonce,
        'merchant_account_id': get_merchant_account(
            code_package.currency
        ),
        'options': {'submit_for_settlement': True},
    })

    if result.is_success:
        code_package.paid = True
        code_package.payment_method = 'braintree'
        code_package.transaction_id = result.transaction.id
        code_package.save()

        generate_codes(code_package)
        send_invoice_email(code_package)
        create_referral_commission(code_package)

        return AJAX_RESULT_SUCCESS, 'Payment processed.'

    return AJAX_RESULT_FAILED, result.message

How Each Payment Flow Operates

Select a gateway to watch its flow animate. Toggle 'Price changed during approval' to see PayPal's re-validation catch a mismatch.

LIVE SIMULATION
Select a payment method to start

PayPal return handler with price re-validation:

Python
def execute_paypal_payment(request):
    payment_id = request.GET.get('paymentId')
    payer_id = request.GET.get('PayerID')
    package_id = request.GET.get('package_id')

    code_package = CodePackage.objects.get(pk=package_id)
    payment = paypalrestsdk.Payment.find(payment_id)

    # Re-validate: price may have changed during redirect
    current_price = code_package.calculated_price
    stored_price = Decimal(
        payment.transactions[0].amount.total
    )

    if current_price != stored_price:
        send_admin_alert(
            f'Price mismatch: {current_price} vs {stored_price}'
        )
        payment.void()
        return redirect_with_error(
            'payment-cancelled'
        )

    if payment.execute({'payer_id': payer_id}):
        code_package.paid = True
        code_package.payment_method = 'paypal'
        code_package.transaction_id = payment_id
        code_package.save()

        generate_codes(code_package)
        send_invoice_email(code_package)
        create_referral_commission(code_package)

        return redirect('payment-success')

    return redirect_with_error('payment-failed')

Edge Cases in Production

Sofort Timeout Recovery

When a Sofort payment is initiated, the code package becomes non-editable. If the user abandons the bank's page or the webhook never arrives, a valid_time timestamp controls when editability is restored. On every subsequent access, the system checks datetime.now() > payment.valid_time and re-enables editing once the timeout passes. No cron job needed.

Decimal Precision for Referrals

Referral commissions are calculated as Decimal(float(amount) * REFERRAL_SHARE), then rounded to cents via int(referral_share * 100) / 100.0. This avoids floating-point drift that could create one-cent discrepancies between the commission record and the actual payout. The same formula runs identically across all three gateway paths.

Idempotent Code Generation

Sofort sends both a 'pending' and a 'received' notification for a single successful payment. The generate_codes() function is idempotent - calling it twice produces the same output. The 'received' webhook safely re-runs generation and marks the package as paid, even if 'pending' already did both. No duplicate codes, no double charges.

Sofort webhook handler with state machine transitions:

Python
@csrf_exempt
def sofort_notification(request):
    xml = xmltodict.parse(request.body)
    txn_id = xml['status_notification']['transaction']
    status = xml['status_notification']['status']

    payment = SofortPayment.objects.get(
        transaction_id=txn_id
    )
    code_package = payment.code_package

    if status == 'pending':
        code_package.paid = True
        code_package.payment_method = 'sofort'
        code_package.save()

        generate_codes(code_package)
        send_invoice_email(code_package)
        create_referral_commission(code_package)

    elif status == 'received':
        # Idempotent: safe to re-run
        generate_codes(code_package)
        code_package.paid = True
        code_package.save()

    elif status == 'loss':
        code_package.editable = False
        code_package.lock_reason = 'Payment failed'
        code_package.save()

    elif status == 'refunded':
        code_package.editable = False
        code_package.lock_reason = 'Payment refunded'
        code_package.save()

    return HttpResponse(status=200)
03.
THE RESULT

Reliable Multi-Gateway Checkout at Scale

The restructured payment layer handled three gateway architectures through a unified pipeline. PayPal price manipulation attempts were caught and logged before execution. Sofort's asynchronous webhooks finalized orders within seconds of bank confirmation. The duplicated finalization code was consolidated, eliminating the drift between gateway paths that had caused intermittent referral calculation errors. The checkout operated continuously across all three rails without a single lost transaction.

KEY METRICS

0Payment Gateways
0ZeroLost Transactions
0%Price Validation
WHAT THE CLIENT SAYS

"We went from debugging payment edge cases every week to a system that just works. The PayPal price validation alone stopped two attempted exploits in the first month."

CTO

E-Commerce SaaS · Digital Products

FAQ

Why not use a payment aggregator like Stripe instead of three separate gateways?

How does the system handle a Sofort webhook that never arrives?

What prevents the referral commission from being calculated twice?

TECHNOLOGY STACK