By then the adapters were proven in production and finalisation lived in one place; replacing three working integrations with one would have been a migration with no visible benefit for customers. A fourth provider, if ever needed, is one more adapter calling the same finalisation.
Services
Backend Engineering, Integration
Industry
E-Commerce SaaS
Year
2022-2024
Three payment contracts, one finalisation path
A SaaS platform sold digital download codes and took money three ways. Braintree answered a card charge in one call. PayPal sent the customer away to approve and back to execute. Sofort redirected to the bank and reported by webhook, sometimes hours later. All three had to finish the same order, and each did it a little differently.
THE CHALLENGE
Three contracts, three copies of the same code
The finalisation logic (generate the codes, send the invoice, calculate the referral commission) existed three times, once per gateway. When one copy was fixed the others drifted, which surfaced as intermittent referral calculation errors. Orders could also get stuck: a PayPal redirect that timed out, a Sofort webhook that never arrived, or a customer who edited the code package mid-payment all left an order in limbo. The widest gap was on the PayPal path: between the customer's approval on paypal.com and the execution call, nothing checked that the order still cost what PayPal had been told. A coupon applied in that window was charged at the old price.
The gateway decides when the money is certain. Finalising the order has to work whenever that is.
THE SOLUTION
One adapter per gateway, one finalisation
Each gateway gets an adapter for its mechanics, and nothing else. Braintree's adapter charges and returns. PayPal's creates the payment, hands off to the redirect and, on return, re-validates the price before it executes. Sofort's creates the transaction, locks the code package against edits and waits for the notification. Every certain charge, whichever adapter produced it, calls the same finalisation: generate codes, send the invoice, book the referral commission. The alternative, a single aggregator, was not available: Stripe did not serve the platform's market at the time, and Sofort was a hard requirement for bank transfers in the DACH region. Adapters cost one failure path per provider; in return the order logic exists once and is tested once.
The PayPal return handler. The comparison happens before execute(), and finalisation is one call:
Python
def finalize_order(code_package, method, transaction_id):
"""Shared by all three gateways."""
code_package.paid = True
code_package.payment_method = method
code_package.transaction_id = transaction_id
code_package.save()
generate_codes(code_package) # idempotent
send_invoice_email(code_package)
create_referral_commission(code_package) # unique per payment
def execute_paypal_payment(request):
payment_id = request.GET.get('paymentId')
payer_id = request.GET.get('PayerID')
code_package = CodePackage.objects.get(
pk=request.GET.get('package_id')
)
payment = paypalrestsdk.Payment.find(payment_id)
# The order may have changed during the 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}):
finalize_order(code_package, 'paypal', payment_id)
return redirect('payment-success')
return redirect_with_error('payment-failed')Run the three flows
Start any gateway; the shared finalisation at the bottom runs once a charge is certain, whichever path produced it. Switch on 'Price changed after approval' and run PayPal again: re-validation voids the payment before execute() and the finalisation never starts.
Live simulationReady
- Tokenise cardSDK nonce, no card data on the server
- Braintree.sale()amount 49.00 EUR, submit_for_settlement
- Create payment
- Redirect to paypal.com
- Customer approves
- Re-validate price
- Execute payment
- Create transaction
- Redirect to bank, lock package
- Wait for webhook
- Webhook: pending
- Webhook: received
Shared finalisationvia Credit card
- generate_codes()50 codes, idempotent
- send_invoice_email()queued
- create_referral_commission()49.00 × 10 % = 4.90 EUR, once per payment
1Finalised orders
0Blocked manipulations
1generate_codes() calls
0Duplicate code sets
The Sofort notification handler. Two notifications for one payment, one idempotent code generation:
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':
finalize_order(code_package, 'sofort', txn_id)
elif status == 'received':
# Arrives after 'pending' for the same payment.
# generate_codes() is idempotent: same codes again.
generate_codes(code_package)
code_package.paid = True
code_package.save()
elif status in ('loss', 'refunded'):
code_package.editable = False
code_package.lock_reason = f'Payment {status}'
code_package.save()
return HttpResponse(status=200)Failure modes
When a flow breaks
A Sofort payment locks the code package the moment it starts, so the customer cannot change what the bank is about to pay for. If the bank page is abandoned or the notification never comes, a valid_time timestamp ends the lock: the next access after it restores editing. No cron job, no stuck orders.
Sofort sends two notifications for one successful payment, 'pending' and 'received', and both may trigger code generation. generate_codes() is idempotent, so the second run produces the same codes, and a unique constraint on the payment reference skips a second commission insert. Two webhooks, one order, no duplicate codes.
The referral commission is computed as Decimal(float(amount) * REFERRAL_SHARE) and rounded to cents with int(referral_share * 100) / 100.0. Floating point would otherwise produce one-cent mismatches between commission record and payout. The formula serves all three gateways because it exists once.
THE RESULT
What the shared path changed
The three gateways still behave like three different companies, but the order no longer cares which one paid. Price manipulation attempts on the PayPal path are caught and logged before execution instead of being discovered in the books. Sofort orders finalise within seconds of the bank's confirmation, whenever that arrives. The intermittent referral errors disappeared with the duplicated code that caused them. The checkout has run across all three rails without losing a transaction.
KEY METRICS
3Payment gateways behind one checkout
0Transactions lost across all three rails
100%PayPal returns price-checked before execution
CLIENT FEEDBACK
"Payment edge cases used to be a weekly debugging topic. Now the three providers run through one path we understand, and the PayPal check stopped two attempted exploits in the first month."
CTO
E-Commerce SaaS · Digital Products
FOR YOUR PROJECT
- When it applies
You take payments through more than one provider, or plan to, and each has its own idea of when a payment is final: instant response, redirect and return, or asynchronous notification.
- What to check
Every place in your code that marks an order as paid, and what runs after it. If that list exists more than once, or a redirect flow executes without comparing current and stored price, you have the two gaps this project closed.
- What it needs
No new infrastructure: one finalisation function that every adapter calls, a stored amount to compare on return, a lock with an expiry for asynchronous flows, and idempotent code generation with a unique constraint on the commission. Here that was a Django backend integration over 2022-2024.
FAQ
TECHNOLOGY STACK
Django
Python
PostgreSQL
Dietmar Eglhofer - CEO, Solvster & VIARESDaniel made a decisive contribution to the founding and implementation of the companies Solvster and VIARES. He was responsible for the entire technical setup of the online presence, including server setups, developing and optimizing the website, and ensuring functionality. I was particularly impressed by how proactively and solution-oriented he always worked on improvements. Daniel was always very responsive and reliable - it was a pleasure working with him. His expertise and commitment contributed significantly to the success of both projects!
Join Dietmar and launch your next project with confidence.
Open to new ProjectsGet In Touch