banking: the caller for Column ACH, with bank accounts, routes, sweep and page - #357
Merged
Merged
Conversation
… and page The Column adapter merged in #318 had no importer: no route, cron or page called getActiveBankProvider(), the bank_transfers migration was never applied to prod, and Railway carried no COLUMN_* variables. This adds the caller and lands the schema. - src/lib/banking/service.ts: linkBankAccount, originateTransfer and sweepBankTransfers. A transfer row is inserted as `initiated` with its idempotency key BEFORE the originator is called, so the unique index is what stops two racing requests originating twice. A provider rejection marks the row failed with the reason; an interrupted origination is re-submitted by the sweep under the same key, which Column honours by returning the original. - The sweep starts a hold at settlement (BANK_TRANSFER_HOLD_DAYS, default 5), completes after it, and re-checks completed transfers daily for sixty days so a late administrative return is recorded as a return. A failed or canceled word from the provider never un-settles moved money. - Counterparties: createCounterparty on the provider interface, the Column POST /counterparties call (lowercase account_type, verified shape), and a bank_counterparties table holding the provider reference, routing number and last four. The account number is never stored. Routing numbers are checked against the ABA checksum before any provider call. - Routes under /api/banking: rail status, accounts (list/link/remove), transfers (list/originate/get). idempotencyKey is required, or an Idempotency-Key header. - /banking page, linked from both header menus. Direction is worded from CoinPay's point of view. The idempotency key is minted when the form opens, so a double submit replays rather than re-originates. - The sweep runs from /api/cron/monitor-payments and reports `skipped` when no originator is configured. - Migrations: bank_transfers (from #314, never applied), ach_payin_hold (from #313, never applied; the cron queried the missing column every tick) and bank_counterparties were applied to prod today. - docs/BANK-TRANSFERS.md and .env.example updated; the doc's Status section had still said the adapter was the next step. Still needed before money moves: a Column production entity and bank account, then COLUMN_API_KEY and COLUMN_BANK_ACCOUNT_ID on Railway. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
ThreatCrush Security Scan331 finding(s) HIGH/CRITICAL: 33 | MEDIUM: 40 | LOW: 258
…and 281 more. Full results in the Security tab. Snippets are redacted; ThreatCrush never prints matched credential material. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What was left off
PR #318 merged the Column ACH adapter on Sep 7, but nothing imported it: no route, cron or page called
getActiveBankProvider(), thebank_transfersmigration was never applied to prod, and Railway had noCOLUMN_*variables. This PR is the caller.What this adds
src/lib/banking/service.ts:linkBankAccount,originateTransfer,sweepBankTransfers. A transfer row is inserted asinitiatedwith its idempotency key before the originator is called, so the unique index is the guard against a double origination under a race. A provider rejection marks the rowfailedwith the reason. An interrupted origination (row with no provider id, older than two minutes) is re-submitted by the sweep under the same key, which Column honours by returning the original transfer./api/cron/monitor-payments. Settlement starts a hold (BANK_TRANSFER_HOLD_DAYS, default 5); the transfer completes after it; completed transfers are re-checked daily for sixty days and a late return is recorded as a return with its code. Afailed/canceledword from the provider never un-settles moved money.createCounterpartyon the provider interface, the ColumnPOST /counterpartiescall, and abank_counterpartiestable holding the provider reference, routing number and last four. The account number is never stored. Routing numbers are checked against the ABA checksum first./api/banking: rail status; accounts list/link/remove; transfers list/originate/get.idempotencyKeyis required (body orIdempotency-Keyheader)./bankingpage, linked from both header menus. The idempotency key is minted when the form opens, so a double submit replays rather than re-originates.bank_transfers(from banking: provider-neutral bank transfers, with Column mapped #314),ach_payin_hold(from stripe: ACH pay-in, held for 24h before the merchant is told #313; the cron had been querying the missing column on every tick) andbank_counterparties..env.exampleupdated.Tests
src/lib/bankingpass (21 new for the service, 3 new for the Column counterparty call).tsc --noEmitexits 0.Not in this PR
COLUMN_BANK_ACCOUNT_IDnames. Until both variables are on Railway,/api/bankingreportsenabled: falseand no money can move.onTransitionhook and the cron logs transitions; nothing downstream consumes bank transfers yet.🤖 Generated with Claude Code