mirror of
https://github.com/hanzo-fi/payments.git
synced 2026-10-30 20:31:20 +00:00
* feat(ee): add Routable as a dedicated EE connector Replaces the standalone connector-routable "protocol conversion" service that exposed Routable through the Generic Connector. The dedicated plugin uses native PSP types (PSPAccount, PSPBalance, PSPPayment, PSPPaymentInitiation) instead of metadata-blob workarounds. Capabilities: FETCH_ACCOUNTS, FETCH_BALANCES, FETCH_EXTERNAL_ACCOUNTS, FETCH_PAYMENTS, CREATE_TRANSFER, CREATE_PAYOUT. Webhooks, bank-account creation, and reversals are deferred to follow-up PRs. See ee/plugins/routable/MAPPING.md for the full Routable<->Formance field mapping and the per-payout metadata key surface. * chore(openapi): regenerate openapi.yaml and api docs for routable connector * fix(openapi): wrap $ref in allOf for nullable PSU fields kin-openapi's stricter OpenAPI 3.1 enforcement now rejects nullable: true as a sibling of $ref. Wrap the PSU contactDetails/address $refs in allOf so nullable becomes a sibling of the parent schema. Same semantics, unblocks just validate-openapi (CI Dirty step). * fix(openapi): drop nullable: true sibling on PSU $ref fields * fix(ee/routable): address CodeRabbit findings - payments cursor: hold the cycle lower bound immutable across all pages of payables and receivables. The previous design advanced LastSeenAt after every page, so page 2 used a tighter floor than page 1 and could silently skip rows. Cycle now uses CycleLowerBound (frozen) and accumulates CycleMaxSeen separately; max is promoted to the next cycle's floor only when receivables exhausts. Legacy state migrates on decode. - client.do: only append the Routable error envelope when the response carried one. Transport errors no longer get a misleading "routable api error: empty body" suffix. - scheme: trim whitespace before classifying delivery_method. - status test: assert PAYMENT_STATUS_REFUNDED is treated as terminal. * fix(routable): update metadata handling and enforce required fields - Updated `line_item_description` to ensure it always has a non-empty value, as required by Routable's v1 schema. - Clarified that `memo` is read-only and not forwarded during payable creation, directing users to use `line_item_description` instead. - Adjusted `send_on` to always be emitted as JSON null when sending immediately, complying with Routable's requirements. - Enhanced error handling in the client to only append the error envelope when content is present, improving log clarity. * feat(routable): add comprehensive field mapping documentation and metadata handling - Introduced MAPPINGS.md as the authoritative reference for the Routable EE connector, detailing the translation of Routable API objects to Formance PSP types. - Added self-describing metadata keys for payments to facilitate correlation between Routable and Formance systems, including `payment_initiation_reference` and `payable_id`. - Implemented tests to ensure the accuracy of metadata handling for payables and receivables, confirming the integrity of the correlation contract. * feat(routable): production hardening for ~200k tx/wk Address Quentin/CodeRabbit feedback on #711 and the production-readiness gaps specific to ~20 tx/min sustained throughput. Critical correctness: - Empty-cycle cursor regression: CycleLowerBound no longer regresses to epoch when both phases of a sync cycle return zero rows. Would have triggered a multi-million-row replay storm at this volume. Engine extension (cross-connector, backward-compatible): - New plugins.RateLimitedError{RetryAfter time.Duration} wraps the existing ErrUpstreamRatelimit sentinel. The activity error translator honours the hint in temporal ApplicationError.NextRetryDelay (clamped to the configured static floor), falling back to the static delay when no hint is present. Every existing connector still satisfies errors.Is(_, ErrUpstreamRatelimit) — no caller changes required. Routable client: - Parses IETF draft-ietf-httpapi-ratelimit-headers RateLimit + RFC 9110 Retry-After per the draft §6 precedence rule (Retry-After wins, else smallest t across listed policies, else nil). Treats 5xx with a RateLimit header as rate-limit-driven (per IETF §6). - HTTP transport pool tuned for sustained ~100-200 req/min: MaxIdleConnsPerHost=64, IdleConnTimeout=90s. Default MaxIdleConnsPerHost=2 would have churned TCP/TLS handshakes at this volume. - PAGE_SIZE bumped from 50 to Routable's documented max of 100 — halves pagination request count at steady state. - Credential probe in Install (GET /v1/settings/accounts?page=1) fails fast on 401/403 instead of letting the worker discover bad creds on the first FETCH_ACCOUNTS run. Plugin architecture: - Mappers extracted into ee/plugins/routable/mappers/ (amounts, scheme, status, metadata + 5 mapping files) per Quentin's review. Pure functions, no plugin state. Plugin package keeps orchestration only. - initiatePayable lifted into payable_create.go; payouts.go and transfers.go are now thin orchestrators sharing the same plumbing. - scheme.go covers every documented Routable delivery_method (ach family, wire, international_wire, check, payout, wallet) with OTHER fallback documented for rails the Formance enum has no dedicated constant for. - status.go drops legacy connector-routable mention and the redundant case "" branch. Tests pinned across the contract: - Idempotency-Key=pi.Reference on createPayout AND createTransfer (cross-flow regression guard). - Pagination invariants: empty-cycle preservation, partial-cycle promotion, multi-page floor immutability, boundary-row replay across cycles, JSON round-trip resumability, legacy-state migration. - 4xx terminal vs 5xx transient classification end-to-end (8 status codes); 503 with vs without RateLimit header; 429 with/without hint; Retry-After delta-seconds + HTTP-date + past-date clamp. - 404 ErrPayableNotFound treated as transient across the eventual- consistency window. - Currency precision JPY (0), USD/EUR (2), KWD/BHD/OMR (3), VND (0). - nil DestinationAccount + nil SourceAccount validation. Docs (MAPPINGS.md): - §1.1 concurrency/immutability statement (Plugin instances are immutable post-construction). - §1.2 credential validation (install-time probe). - §3.6 cursor invariants — three explicit invariants (resumable, lossless, tiebreaker) replacing the deprecated lastSeenAt language. - §5.5 payout-under-retries lifecycle mermaid + rate-limit hint explainer. - §6 ops capacity & cost model — Routable RPS budget, Temporal workflow/activity volumes, Cloud Action estimate (~24M/month ⇒ ~$600 at list pricing), and the four levers operators can pull (polling period, page size, webhooks follow-up, per-capability schedules follow-up). * test(routable): add direct unit tests (mappers 31% -> 90%, client 62% -> 70%, plugins/errors 12% -> 22%) * fix(routable): branch on 201/202 HTTP status (not on body) for payable creation, validate inputs at the choke point, trim whitespace credentials * chore(routable): trim doc-comments duplicating MAPPINGS.md * chore(routable): scrub rate-limit narrative from MAPPINGS.md and client.go doc-comments Removes the documentation references to the IETF/RFC 9110 rate-limit hint parsing that lived briefly in this branch. The shared parser was lifted into the engine's httpwrapper package in #715; this PR no longer touches the engine or any internal/ code. * chore(routable): regenerate SDK gen.lock after rebase onto main * fix(routable): non-retriable validation errors, PI-Payment linkage on terminal failures, status_changed_at timestamps, externally_paid mapping * fix(routable): always return Payment from pollPayableStatus; resync MAPPINGS volume model * fix(routable): inline time.Now() in external_accounts, return req.State unchanged on throttle skip * Add PayoutsPerSecond() to Routable connector --------- Co-authored-by: Crimson Thompson <[email protected]>
3.1 KiB
3.1 KiB
V3ConnectorConfig
Supported Types
V3AdyenConfig
v3ConnectorConfig := components.CreateV3ConnectorConfigAdyen(components.V3AdyenConfig{/* values here */})
V3AtlarConfig
v3ConnectorConfig := components.CreateV3ConnectorConfigAtlar(components.V3AtlarConfig{/* values here */})
V3BankingbridgeConfig
v3ConnectorConfig := components.CreateV3ConnectorConfigBankingbridge(components.V3BankingbridgeConfig{/* values here */})
V3BankingcircleConfig
v3ConnectorConfig := components.CreateV3ConnectorConfigBankingcircle(components.V3BankingcircleConfig{/* values here */})
V3BitstampConfig
v3ConnectorConfig := components.CreateV3ConnectorConfigBitstamp(components.V3BitstampConfig{/* values here */})
V3CoinbaseprimeConfig
v3ConnectorConfig := components.CreateV3ConnectorConfigCoinbaseprime(components.V3CoinbaseprimeConfig{/* values here */})
V3ColumnConfig
v3ConnectorConfig := components.CreateV3ConnectorConfigColumn(components.V3ColumnConfig{/* values here */})
V3CurrencycloudConfig
v3ConnectorConfig := components.CreateV3ConnectorConfigCurrencycloud(components.V3CurrencycloudConfig{/* values here */})
V3DummypayConfig
v3ConnectorConfig := components.CreateV3ConnectorConfigDummypay(components.V3DummypayConfig{/* values here */})
V3FireblocksConfig
v3ConnectorConfig := components.CreateV3ConnectorConfigFireblocks(components.V3FireblocksConfig{/* values here */})
V3GenericConfig
v3ConnectorConfig := components.CreateV3ConnectorConfigGeneric(components.V3GenericConfig{/* values here */})
V3IncreaseConfig
v3ConnectorConfig := components.CreateV3ConnectorConfigIncrease(components.V3IncreaseConfig{/* values here */})
V3MangopayConfig
v3ConnectorConfig := components.CreateV3ConnectorConfigMangopay(components.V3MangopayConfig{/* values here */})
V3ModulrConfig
v3ConnectorConfig := components.CreateV3ConnectorConfigModulr(components.V3ModulrConfig{/* values here */})
V3MoneycorpConfig
v3ConnectorConfig := components.CreateV3ConnectorConfigMoneycorp(components.V3MoneycorpConfig{/* values here */})
V3PlaidConfig
v3ConnectorConfig := components.CreateV3ConnectorConfigPlaid(components.V3PlaidConfig{/* values here */})
V3PowensConfig
v3ConnectorConfig := components.CreateV3ConnectorConfigPowens(components.V3PowensConfig{/* values here */})
V3QontoConfig
v3ConnectorConfig := components.CreateV3ConnectorConfigQonto(components.V3QontoConfig{/* values here */})
V3RoutableConfig
v3ConnectorConfig := components.CreateV3ConnectorConfigRoutable(components.V3RoutableConfig{/* values here */})
V3StripeConfig
v3ConnectorConfig := components.CreateV3ConnectorConfigStripe(components.V3StripeConfig{/* values here */})
V3TinkConfig
v3ConnectorConfig := components.CreateV3ConnectorConfigTink(components.V3TinkConfig{/* values here */})
V3WiseConfig
v3ConnectorConfig := components.CreateV3ConnectorConfigWise(components.V3WiseConfig{/* values here */})