Files
f755e1a3a4 feat(bitstamp): EN-1015 add orders + conversions and refactor for production-readiness (#724)
* feat(bitstamp): EN-1015 add orders + conversions and refactor for production-readiness

Adds first-class orders and conversions primitives to the Bitstamp connector and refactors the package along Formance conventions. The HTTP client is split into client/types.go + client/error.go with OpenTelemetry instrumentation and a typed DerivativesUnsupportedError that drives a process-lifetime try-and-skip cache for permission-gated endpoints (my_markets, fees, crypto-transactions). State is moved to opaque cursors with backward-compatible decoders so existing installs keep their watermarks. Payments are now a three-source union (user_transactions + crypto-transactions + withdrawal-requests) with fail-whole-cycle semantics on top-level source errors and log+continue on per-row map errors. Orders reconcile open_orders snapshots with order_status; conversions detect two-asset user_transactions.type=36 (instant buy/sell). Accounts surface the currency ticker as Name; sub-account transfers (types 14/33/35) are modelled as linked PAYOUT/PAYIN pairs correlated via metadata, reflecting Bitstamp's one-API-key-per-account scope. The Temporal task tree nests balances under accounts (Qonto FromPayload pattern) with payments, orders and conversions as independent roots. Install-time enrichment loads markets, my_markets, trading fees and withdrawal fees in parallel via a generic refreshCache[T] helper with TTL. Full transformation logic lives in a dedicated mappers/ subpackage with extensive unit coverage. MAPPINGS.md documents every decision, the error-policy matrix, and the known historical-orders gap.

* fix(bitstamp): address PR #724 CodeRabbit review — evict on order_status error, deterministic enrichment, log levels

Evict retention-expired tracked orders in fetchNextOrders when GetOrderStatus errors out: a stale ID past the 30-day window will keep failing the call indefinitely, growing state unbounded and spamming Error logs every cycle. Log the eviction at Info because the failure is expected for retention-expired entries. Make representative market/trading-fee selection in buildEnrichmentForCurrency deterministic by picking the lexicographically-smallest match on a composite key (CounterCurrency|MarketType for markets, CurrencyPair for fees) to prevent account-metadata flapping if the PSP returns rows reordered between cycles. Downgrade the derivatives-row skip log in fetchNextConversions from Errorf to Infof — it is an expected branch on a spot-only connector, not a fault. Drop a redundant AbsAmount wrap inside an IsZeroAmount call (Sign() already handles negatives natively). Add language tag to a MAPPINGS.md fenced block to satisfy markdownlint. New unit tests cover the eviction-on-error path and lock the deterministic-representative contract via permutation testing.

Co-authored-by: Cursor <[email protected]>

* Remove any cache initializations from Install step, remove useless skip behaviour for crypto-transactions and withdrawals

* Use GET request for my_markets endpoint, and only handle derivative not supported error where likely to occur

* Drastically reduce amount of caching - instead fetch enrichment data only when new accounts are found

* Delay fetch currencies calls until after we know if work needs to be done to reduce unneeded requests in inactive accounts

* Revert changes to balance fetching that made it only fetch balance once

* Import transactions with trailing zeros that exceed the currency's reported precision

* One exclusive decimal amount parser function among different functions

* Allow two orders with the same ID to be present in a slice by combining them into one record with multiple adjustments

* Use CreatedAt to determine order or order adjustments

* Make Orders workflow a child of accounts, and search markets related to the currency of the account

* Enhance metadata returned in orders

* Only save real account IDs related to orders

* Import each order event only once via the source currency

* Regenerate capabilities

* Add paging mechanism and fix expectations about market event IDs

* Export even if account is destination account

* Address coderabbit comments

* Fix comments on workflow header

* Address coderabbit comments regarding models order

* Only use user transactions endpoint to fetch both conversions and payments

* Truncate trailing digits when they exceed the precision bitstamp provided - raw values still preserved in raw payload

---------

Co-authored-by: Cursor <[email protected]>
Co-authored-by: Crimson Thompson <[email protected]>
2026-05-28 08:40:34 +00:00

162 lines
5.6 KiB
Go

// Code generated by MockGen. DO NOT EDIT.
// Source: client.go
//
// Generated by this command:
//
// mockgen -source client.go -destination client_generated.go -package client . Client
//
// Package client is a generated GoMock package.
package client
import (
context "context"
reflect "reflect"
gomock "go.uber.org/mock/gomock"
)
// MockClient is a mock of Client interface.
type MockClient struct {
ctrl *gomock.Controller
recorder *MockClientMockRecorder
isgomock struct{}
}
// MockClientMockRecorder is the mock recorder for MockClient.
type MockClientMockRecorder struct {
mock *MockClient
}
// NewMockClient creates a new mock instance.
func NewMockClient(ctrl *gomock.Controller) *MockClient {
mock := &MockClient{ctrl: ctrl}
mock.recorder = &MockClientMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockClient) EXPECT() *MockClientMockRecorder {
return m.recorder
}
// GetAccountBalances mocks base method.
func (m *MockClient) GetAccountBalances(ctx context.Context) ([]AccountBalance, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetAccountBalances", ctx)
ret0, _ := ret[0].([]AccountBalance)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// GetAccountBalances indicates an expected call of GetAccountBalances.
func (mr *MockClientMockRecorder) GetAccountBalances(ctx any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAccountBalances", reflect.TypeOf((*MockClient)(nil).GetAccountBalances), ctx)
}
// GetAccountOrderData mocks base method.
func (m *MockClient) GetAccountOrderData(ctx context.Context, market string, sinceID *string) ([]AccountOrderDataEvent, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetAccountOrderData", ctx, market, sinceID)
ret0, _ := ret[0].([]AccountOrderDataEvent)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// GetAccountOrderData indicates an expected call of GetAccountOrderData.
func (mr *MockClientMockRecorder) GetAccountOrderData(ctx, market, sinceID any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAccountOrderData", reflect.TypeOf((*MockClient)(nil).GetAccountOrderData), ctx, market, sinceID)
}
// GetCurrencies mocks base method.
func (m *MockClient) GetCurrencies(ctx context.Context) ([]Currency, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetCurrencies", ctx)
ret0, _ := ret[0].([]Currency)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// GetCurrencies indicates an expected call of GetCurrencies.
func (mr *MockClientMockRecorder) GetCurrencies(ctx any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetCurrencies", reflect.TypeOf((*MockClient)(nil).GetCurrencies), ctx)
}
// GetMarkets mocks base method.
func (m *MockClient) GetMarkets(ctx context.Context) ([]Market, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetMarkets", ctx)
ret0, _ := ret[0].([]Market)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// GetMarkets indicates an expected call of GetMarkets.
func (mr *MockClientMockRecorder) GetMarkets(ctx any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetMarkets", reflect.TypeOf((*MockClient)(nil).GetMarkets), ctx)
}
// GetMyMarkets mocks base method.
func (m *MockClient) GetMyMarkets(ctx context.Context) ([]MyMarket, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetMyMarkets", ctx)
ret0, _ := ret[0].([]MyMarket)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// GetMyMarkets indicates an expected call of GetMyMarkets.
func (mr *MockClientMockRecorder) GetMyMarkets(ctx any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetMyMarkets", reflect.TypeOf((*MockClient)(nil).GetMyMarkets), ctx)
}
// GetTradingFees mocks base method.
func (m *MockClient) GetTradingFees(ctx context.Context) ([]TradingFee, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetTradingFees", ctx)
ret0, _ := ret[0].([]TradingFee)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// GetTradingFees indicates an expected call of GetTradingFees.
func (mr *MockClientMockRecorder) GetTradingFees(ctx any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetTradingFees", reflect.TypeOf((*MockClient)(nil).GetTradingFees), ctx)
}
// GetUserTransactions mocks base method.
func (m *MockClient) GetUserTransactions(ctx context.Context, sinceID *int64, limit int) ([]UserTransaction, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetUserTransactions", ctx, sinceID, limit)
ret0, _ := ret[0].([]UserTransaction)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// GetUserTransactions indicates an expected call of GetUserTransactions.
func (mr *MockClientMockRecorder) GetUserTransactions(ctx, sinceID, limit any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetUserTransactions", reflect.TypeOf((*MockClient)(nil).GetUserTransactions), ctx, sinceID, limit)
}
// GetWithdrawalFees mocks base method.
func (m *MockClient) GetWithdrawalFees(ctx context.Context) ([]WithdrawalFee, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetWithdrawalFees", ctx)
ret0, _ := ret[0].([]WithdrawalFee)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// GetWithdrawalFees indicates an expected call of GetWithdrawalFees.
func (mr *MockClientMockRecorder) GetWithdrawalFees(ctx any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetWithdrawalFees", reflect.TypeOf((*MockClient)(nil).GetWithdrawalFees), ctx)
}