billing: pass idempotency_key to HCB disbursements once HCB + hcbv4 support it
- Dominant language
- Ruby
- Stars
- 24
- Forks
- 18
- Avg merge
- 7h 11m
- Merged PRs (30d)
- 7
Description
## context
The billing ledger rework (branch `ui2-fr`) creates an `HCB::Transfer` row with a unique `idempotency_key` *before* every disbursement call, and embeds it in the disbursement name and custom memo. But HCB's v4 `POST /organizations/:id/transfers` currently has no idempotency support, so a timeout / 5xx after HCB committed the disbursement leaves the transfer in `unknown` and `Billing::Reconciler` has to prove the outcome by matching amount + direction + counterparty against the HQ org's transactions.
That reconciler has one known gap: a human manually sending HQ the identical amount from the same org during the unknown window could be mis-matched. An idempotency key on HCB's side closes it.
## blocked on
- [ ] HCB PR: `idempotency_key` on `POST /organizations/:id/transfers` (unique per source org; replay returns the existing disbursement)
- [ ] hcbv4 PR: `create_disbursement(..., idempotency_key:)`
## when those land
- [ ] bump `hcbv4` in Gemfile
- [ ] `Billing::Executor`: pass `idempotency_key: transfer.idempotency_key` on both debit (`BillingProfile#create_disbursement!`) and credit (`BillingProfile.refund_to_organization!`) paths
- [ ] `Billing::Executor`: an `unknown` transfer becomes safely retryable — reclassify timeouts / 5xx as `failed` with backoff instead of `unknown` **only when** the key was sent
- [ ] keep `Billing::Reconciler` for the "HCB succeeded, we crashed before writing `remote_id`" case and for the historical backfill; it just stops being load-bearing
- [ ] update `spec/services/billing/executor_spec.rb` classification cases
Contributor guide
Research direction
Start with the blocked HCB and hcbv4 prerequisites, then read Gemfile, Billing::Executor, BillingProfile#create_disbursement!, and BillingProfile.refund_to_organization!. Run spec/services/billing/executor_spec.rb, focusing on classification cases. Done means the dependency is bumped, both paths pass the idempotency key, retry classification is covered, and reconciler behavior remains covered.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- ruby
- Domain
- backend, payments
- Issue type
- Feature
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Active
- Clarity
- Mostly clear
- Newbie friendliness
- 38/100