rustlavel-payment
Payment gateways behind one trait, for the Rustlavel framework: charges over virtual account, QRIS, e-wallet and retail; transfers out; and a webhook receiver that verifies the signature and refuses to hear the same payment twice.
The shape
use ;
let charge = gateway
.create_charge
.await?;
// charge.instructions.account_number is what the customer transfers to.
Money is an integer of minor units — never a float, because money that is rounded is money that goes missing. Channel is an enum, so a typo in a bank code is a compile error rather than a charge nobody can pay.
The receiver, and why it deduplicates on disk
A gateway retries a webhook until it hears an acknowledgement, and a network that drops the acknowledgement delivers the same payment twice. The receiver does three things in an order that matters:
- Verify the signature. Over the raw bytes, never parsed JSON — re-serialising reorders keys and the signature no longer matches. Anything that fails is
401and is not parsed further. - Record the event by the gateway's id. A duplicate is answered
200without running the handler: the gateway needs to hear the acknowledgement it missed. - Run the handler. If it fails, answer
500so the gateway retries — and withdraw the record, so the retry is processed rather than dropped as a duplicate of something nothing handled.
let receiver = new;
r.post;
The record has to survive a restart, because a retry that arrives after a deploy is still a duplicate. DatabaseWebhookLog (behind the db feature) is one INSERT against a unique index — sixteen deliveries racing, exactly one told it was first, measured against PostgreSQL. MemoryWebhookLog is for tests. database::schema creates the table; call it from a migration.
No driver is invented
FakeGateway is the only driver today. It keeps everything in memory, signs its callbacks the way a real gateway does, and refuses a second transfer under a reference it has seen — so a test cannot pass while relying on behaviour production would not have. mark_paid is the customer; webhook_for is the callback.
A driver for a real gateway is written against that gateway's published specification. An adapter written from what its competitors do is code that looks finished and fails on the first real callback.
Licence
MIT.