Skip to content

Every pull request has a story.

WellMerged orders each change, checks it against your standards, and maps what it can break. GitHub stays the source of truth.

acme/ledger#482

Add idempotent refund endpoint with retry-safe webhooks

1 of 4 Stories reviewed

Blocked by 1 failing required check and 1 critical finding

Risk high · effort 3/5, about 25 min

68 blast radius

1,284 files scanned

Story 2 in focus: highlighted edges show every file its change reaches. Select any file to trace it.

refunds.py ledger.py webhooks.py 0042_refund_idempotenc… app.py payouts.py exports.py reconcile.py webhooks.py invoices.py scheduler.py statements.py test_refunds.py test_ledger.py RefundDialog.tsx ChargeDrawer.tsx
  • Changed
  • Direct dependent
  • Transitive dependent
  • Entry point
  • Test

One pull request, start to merge.

Read it in the order it makes sense.

Hunks are grouped into Stories by intent and ordered foundation first. Each Story explains the change, cites the lines, and asks the questions only a person can answer.

Open this in the demo

Story 2 of 4

Accept refunds through an idempotent endpoint

POST /v1/refunds looks up the key first, locks the charge, checks the refundable balance, then writes the request and the ledger entry.

What a human should decide

  • Can two requests with different keys refund past the balance before the lock is held?
  • Should the HMAC secret come from configuration instead of source?

services/api/payments/refunds.py

      
17 + @router.post("", response_model=RefundResponse, status_code=201)
18 + async def create_refund(
19 + body: RefundRequest,
20 + idempotency_key: str = Header(alias="Idempotency-Key"),
21 + store: RefundStore = Depends(get_store),
22 + ) -> RefundResponse:
23 + existing = await store.find_by_key(body.merchant_id, idempotency_key)
24 + if existing is not None:
25 + return RefundResponse.from_row(existing)
26 + charge = await store.lock_charge(body.charge_id)
27 + if body.amount_cents > charge.amount_cents - charge.refunded_cents:
28 + raise RefundRejected("amount exceeds refundable balance")
29 + row = await store.insert_request(body, idempotency_key)
30 + await post_refund_entry(charge, body.amount_cents)
31 + return RefundResponse.from_row(row)

This route moves money. Two lines in it would let anyone move it.

One injectable SQL statement and one committed secret sit on a public, money-moving route. Both are one-line fixes.

Security review runs on every pull request, and recurring scans cover the whole repository between them.

See the repository scans in the demo
A shield formed from code brackets with a keyhole at its centre, standing over three diff lines

Controls a security team can sign off on.

Open the admin console in the demo
Single sign-on
SAML through your identity provider, verified before it goes live, with SSO enforcement per domain.
Provisioning
SCIM 2.0 users and groups. Removing someone revokes their access and their API keys.
Roles
Owner, admin, member and viewer, mapped from identity-provider groups and enforced on every route.
Audit
Overrides, policy changes, role changes, scans and key events, exportable to CSV.
API
Scoped keys shown once and stored as hashes, with usage by day.
Policy
Enforced checks, required override reasons, and auto-approval in shadow before live, with a kill switch.
Sandboxes
Test runs in isolated containers with no network, resource limits and a plan you approve first.
Deployment
Hosted, dedicated tenant or your own cloud, a choice of data region, and your own model provider.

Every pull request has a story. Read yours.

Install the GitHub App, open a pull request, and review it in the order it makes sense.

Two paths merging into one bold lime path that ends in a checkmark