GoodLeap · 2026
Two prices, one invoice, and every way it can half-fail
Built the pricing calculation, reconciliation, and retry flows for dual pricing — a marked-up card price beside a base bank-transfer price, and the failure states that creates.
On requestThe situation
Dual pricing shows a customer two numbers: a base price if they pay by bank transfer, and a marked-up price if they pay by card. Simple to describe, and it touches pricing, webhooks, list endpoints, receipt emails, retries, and analytics.
The hard part is that the price is not known until the payment method is known, and the payment method is not known until the payment either succeeds or fails.
The constraints
- An invoice can combine a one-off transaction and a subscription, each of which can succeed or fail independently — so partial failure is not an edge case, it is a set of states that all have to be defined.
- A failed bank payment has to be repriced correctly before it can be retried by card, including resetting the amount held on the payment intent.
- Downstream analytics consumed these events, so the event contract was a public interface with other teams behind it.
The call
Derive the displayed price on the backend from the payment method that actually settled, rather than letting each client work it out — one shared derivation, instead of every surface holding its own idea of which price to show.
Enumerate all three partial-failure combinations explicitly and handle each, instead of treating combined invoices as a happy path with exceptions.
Treat the event schema change as a contract change — document it and tell the consuming team — which is also what prompted me to propose a schema registry, since convention was the only thing keeping that contract honest.
What happened
Dual pricing shipped across pricing, reconciliation, list endpoints, receipts, retries, and the downstream event contract.
The event-contract question outlived the feature and became a written proposal for validating payment events against a registered schema in CI.
What I'd do differently
Price derivation should have been one piece of shared logic from the start. I only named the underlying cause because the same ticket kept coming back wearing a different hat — and it was the naming, not any single fix, that made it stop.