Skip to content

feat(api): add account-to-account transfer endpoint#19

Merged
marioalvial merged 2 commits intomainfrom
feat/transfer-operation
May 6, 2026
Merged

feat(api): add account-to-account transfer endpoint#19
marioalvial merged 2 commits intomainfrom
feat/transfer-operation

Conversation

@marioalvial
Copy link
Copy Markdown
Contributor

Description

Adds POST /api/operations/transfer — a new operation type for moving funds between two accounts of the same customer. The referenced quote determines whether it is a same-asset book transfer (1:1 spot) or a cross-asset FX transfer (e.g. BRL on the source account → USD on the target). No external rail is involved.

The shape mirrors the existing operation siblings (withdrawal/swap/deposit): accountId on the operation root represents the source account, the target is carried as a snapshot on intent.targetAccount — symmetric to how withdrawal carries intent.beneficiary and deposit carries intent.fundingInstruction.

Cross-customer transfers are intentionally out of scope for this slice — same pattern as withdrawal shipping REFERENCE-only first.

Key Changes

  • apis/fx-payment/openapi.yml — new path POST /api/operations/transfer with full 201/400/401/404/409/422 coverage; new schemas CreateTransferRequest (sourceAccountId, targetAccountId, quoteId) and OperationTransferIntent; OperationIntent oneOf + discriminator.mapping extended with TRANSFER; OperationResponse.description updated to list transfers as the 4th sibling.
  • New shared example TargetAccountNotFound — added to components.examples mirroring the canonical RESOURCE_NOT_FOUND shape with the target-account UUID. Replaces an inline example that used a non-existent ACCOUNT_NOT_FOUND code (caught during simplify review).
  • Distinct error codes SOURCE_ASSET_NOT_ENABLED / TARGET_ASSET_NOT_ENABLED — chosen over reusing withdrawal's INVALID_SOURCE_ASSET, which has different semantics (quote-vs-instruction mismatch) and a different details payload. The new codes describe the account-capability check more precisely and mirror deposit's RAIL_NOT_AVAILABLE style.
  • journeys/transfer.mdx — new guide following the journey template (Overview, Prerequisites, Steps, What happens next), with same-asset and cross-asset examples and a Warning on the cross-customer constraint.
  • api-reference/fx-payment/operations/create-transfer.mdx — one-line OpenAPI stub registering the auto-generated reference page.
  • docs.jsonjourneys/transfer added to the Journeys group; api-reference/fx-payment/operations/create-transfer added to the API Reference → Operations group.

Type of change

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • This change requires a documentation update

How Has This Been Tested?

  • Unit Test
  • Integration Test

Validated locally with mint validate (passes) and mint broken-links (passes). Three independent review agents (reuse / quality / efficiency) ran against the diff before commit; all consensus findings were applied — including the wrong-error-code bug and the prose-trim wins. The remaining suggestions (extracting MissingQuoteId to a shared component, extracting a track-operation snippet) touch all four operation endpoints and are out of scope for this PR.

Checklist:

  • My code follows the style guidelines of this project
  • I have performed a self-review of my own code
  • I have commented my code, particularly in hard-to-understand areas
  • I have made corresponding changes to the documentation
  • My changes generate no new warnings
  • I have added tests that prove my fix is effective or that my feature works
  • New and existing unit tests pass locally with my changes
  • Any dependent changes have been merged and published in downstream modules
  • I have checked my code and corrected any misspellings

Backend / infra follow-up

The live Intent sealed class in fx-payment/core/.../domain/Intent.kt currently has only Withdrawal | Deposit | Swap. Implementing this contract in code requires:

  1. New data class Transfer(val targetAccount: AccountReference) : Intent() variant on the sealed class, plus a TransferPlanner (sibling of WithdrawalPlanner / SwapPlanner / DepositPlanner).
  2. New executor for the source-debit + target-credit ledger move (single-customer book transfer) — same-asset path bypasses the FX engine; cross-asset path reuses the swap-style FX leg.
  3. New error codes wired into the validation layer: SAME_SOURCE_AND_TARGET, TARGET_ACCOUNT_NOT_ACTIVE, SOURCE_ASSET_NOT_ENABLED, TARGET_ASSET_NOT_ENABLED.
  4. OPERATION_REQUESTED webhook event fires on creation as for the other three operations; lifecycle transitions remain unpublished.
  5. The existing aspirational endpoints in docs ahead of implementation convention applies — this PR can merge before the code lands.

Adds POST /api/operations/transfer for moving funds between two accounts
of the same customer. The referenced quote determines whether it is a
same-asset book transfer (1:1 spot) or a cross-asset FX transfer.

- New OpenAPI path with 201/400/401/404/409/422 responses
- New schemas: CreateTransferRequest, OperationTransferIntent
- OperationIntent oneOf + discriminator extended with TRANSFER variant
- New shared example: TargetAccountNotFound (canonical RESOURCE_NOT_FOUND
  shape, target-account UUID)
- Distinct error codes SOURCE_ASSET_NOT_ENABLED and TARGET_ASSET_NOT_ENABLED
  (capability check on accounts, separate from withdrawal's
  INVALID_SOURCE_ASSET which is a quote-vs-instruction mismatch)
- Journey guide journeys/transfer.mdx (3 steps: quote, transfer, track)
- API Reference stub api-reference/fx-payment/operations/create-transfer.mdx
- Navigation entries in docs.json
@mintlify
Copy link
Copy Markdown

mintlify Bot commented May 6, 2026

Preview deployment for your docs. Learn more about Mintlify Previews.

Project Status Preview Updated (UTC)
tracefinance 🟢 Ready View Preview May 6, 2026, 4:48 PM

💡 Tip: Enable Workflows to automatically generate PRs for you.

Same-asset transfers no longer require a pre-created quote. The request
takes amount {value, asset} directly; the system generates a 1:1 quote
internally and returns it on the response so the OperationResponse shape
stays unchanged. quoteId remains accepted as an optional field, reserved
for future use.

- CreateTransferRequest: amount is required, quoteId is optional
- Path description and journey updated to remove the quote prerequisite
- Journey collapses from 3 steps to 2 (transfer, track)
- 201 response example shows the system-generated 1:1 quote
- 400 missingQuoteId replaced with missingAmount
- 404 quoteNotFound dropped (no customer-supplied quote to look up)
- 422 quoteExpired/quoteAlreadyConsumed dropped (no quote consumption)
- Asset-not-enabled details key normalised from quoteSourceAsset/
  quoteTargetAsset to asset (single asset on the wire)
- OperationResponse.quote stays required; description notes the
  transfer-side generation
@marioalvial marioalvial merged commit 7c3490e into main May 6, 2026
9 checks passed
@marioalvial marioalvial deleted the feat/transfer-operation branch May 6, 2026 18:34
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants