diff --git a/components/ledger/internal/api/v2/query.go b/components/ledger/internal/api/v2/query.go index d7d87581f6..dab68f4935 100644 --- a/components/ledger/internal/api/v2/query.go +++ b/components/ledger/internal/api/v2/query.go @@ -4,6 +4,7 @@ import ( "net/http" "strings" + "github.com/formancehq/stack/libs/go-libs/api" "github.com/formancehq/stack/libs/go-libs/bun/bunpaginate" "github.com/formancehq/ledger/internal/engine/command" @@ -28,10 +29,8 @@ func getCommandParameters(r *http.Request) command.Parameters { dryRunAsString := r.URL.Query().Get("dryRun") dryRun := strings.ToUpper(dryRunAsString) == "YES" || strings.ToUpper(dryRunAsString) == "TRUE" || dryRunAsString == "1" - idempotencyKey := r.Header.Get("Idempotency-Key") - return command.Parameters{ DryRun: dryRun, - IdempotencyKey: idempotencyKey, + IdempotencyKey: api.IdempotencyKeyFromRequest(r), } } diff --git a/ee/wallets/openapi.yaml b/ee/wallets/openapi.yaml index e38899aa5c..36aaea0866 100644 --- a/ee/wallets/openapi.yaml +++ b/ee/wallets/openapi.yaml @@ -191,6 +191,12 @@ paths: operationId: updateWallet tags: - Wallets + parameters: + - name: Idempotency-Key + in: header + description: Use an idempotency key + schema: + type: string requestBody: content: application/json: @@ -323,6 +329,11 @@ paths: required: true schema: type: string + - name: Idempotency-Key + in: header + description: Use an idempotency key + schema: + type: string post: summary: Debit a wallet operationId: debitWallet @@ -356,6 +367,11 @@ paths: required: true schema: type: string + - name: Idempotency-Key + in: header + description: Use an idempotency key + schema: + type: string post: summary: Credit a wallet operationId: creditWallet @@ -467,6 +483,11 @@ paths: required: true schema: type: string + - name: Idempotency-Key + in: header + description: Use an idempotency key + schema: + type: string requestBody: content: application/json: @@ -493,6 +514,11 @@ paths: required: true schema: type: string + - name: Idempotency-Key + in: header + description: Use an idempotency key + schema: + type: string post: summary: Cancel a hold operationId: voidHold @@ -974,6 +1000,7 @@ components: expiresAt: type: string format: date-time + nullable: true priority: type: integer format: bigint diff --git a/ee/wallets/pkg/api/handler_balances_create_test.go b/ee/wallets/pkg/api/handler_balances_create_test.go index e072caf76d..b2277201b2 100644 --- a/ee/wallets/pkg/api/handler_balances_create_test.go +++ b/ee/wallets/pkg/api/handler_balances_create_test.go @@ -76,7 +76,7 @@ func TestBalancesCreate(t *testing.T) { appliedMetadata map[string]string ) testEnv := newTestEnv( - WithAddMetadataToAccount(func(ctx context.Context, ledger, account string, metadata map[string]string) error { + WithAddMetadataToAccount(func(ctx context.Context, ledger, account, ik string, metadata map[string]string) error { targetedLedger = ledger targetedAccount = account appliedMetadata = metadata diff --git a/ee/wallets/pkg/api/handler_holds_confirm.go b/ee/wallets/pkg/api/handler_holds_confirm.go index d2b66e376d..93ce9e2194 100644 --- a/ee/wallets/pkg/api/handler_holds_confirm.go +++ b/ee/wallets/pkg/api/handler_holds_confirm.go @@ -5,6 +5,8 @@ import ( "math/big" "net/http" + "github.com/formancehq/stack/libs/go-libs/api" + wallet "github.com/formancehq/wallets/pkg" "github.com/go-chi/chi/v5" "github.com/go-chi/render" @@ -28,7 +30,7 @@ func (m *MainHandler) confirmHoldHandler(w http.ResponseWriter, r *http.Request) } } - err := m.manager.ConfirmHold(r.Context(), wallet.ConfirmHold{ + err := m.manager.ConfirmHold(r.Context(), api.IdempotencyKeyFromRequest(r), wallet.ConfirmHold{ HoldID: chi.URLParam(r, "holdID"), Amount: big.NewInt(data.Amount), Final: data.Final, diff --git a/ee/wallets/pkg/api/handler_holds_confirm_test.go b/ee/wallets/pkg/api/handler_holds_confirm_test.go index 1d59f91dfd..5bddef366f 100644 --- a/ee/wallets/pkg/api/handler_holds_confirm_test.go +++ b/ee/wallets/pkg/api/handler_holds_confirm_test.go @@ -40,7 +40,7 @@ func TestHoldsConfirm(t *testing.T) { Balances: balances, }, nil }), - WithCreateTransaction(func(ctx context.Context, name string, postTransaction wallet.PostTransaction) (*shared.V2Transaction, error) { + WithCreateTransaction(func(ctx context.Context, name, ik string, postTransaction wallet.PostTransaction) (*shared.V2Transaction, error) { compareJSON(t, wallet.PostTransaction{ Script: &shared.V2PostTransactionScript{ Plain: wallet.BuildConfirmHoldScript(false, "USD"), @@ -95,7 +95,7 @@ func TestHoldsPartialConfirm(t *testing.T) { }, }, nil }), - WithCreateTransaction(func(ctx context.Context, name string, postTransaction wallet.PostTransaction) (*shared.V2Transaction, error) { + WithCreateTransaction(func(ctx context.Context, name, ik string, postTransaction wallet.PostTransaction) (*shared.V2Transaction, error) { compareJSON(t, wallet.PostTransaction{ Script: &shared.V2PostTransactionScript{ Plain: wallet.BuildConfirmHoldScript(false, "USD"), @@ -230,7 +230,7 @@ func TestHoldsPartialConfirmWithFinal(t *testing.T) { }, }, nil }), - WithCreateTransaction(func(ctx context.Context, name string, script wallet.PostTransaction) (*shared.V2Transaction, error) { + WithCreateTransaction(func(ctx context.Context, name, ik string, script wallet.PostTransaction) (*shared.V2Transaction, error) { compareJSON(t, wallet.PostTransaction{ Script: &shared.V2PostTransactionScript{ Plain: wallet.BuildConfirmHoldScript(true, "USD"), diff --git a/ee/wallets/pkg/api/handler_holds_void.go b/ee/wallets/pkg/api/handler_holds_void.go index 4ae66dd061..52aca4f48c 100644 --- a/ee/wallets/pkg/api/handler_holds_void.go +++ b/ee/wallets/pkg/api/handler_holds_void.go @@ -4,12 +4,14 @@ import ( "errors" "net/http" + "github.com/formancehq/stack/libs/go-libs/api" + wallet "github.com/formancehq/wallets/pkg" "github.com/go-chi/chi/v5" ) func (m *MainHandler) voidHoldHandler(w http.ResponseWriter, r *http.Request) { - err := m.manager.VoidHold(r.Context(), wallet.VoidHold{ + err := m.manager.VoidHold(r.Context(), api.IdempotencyKeyFromRequest(r), wallet.VoidHold{ HoldID: chi.URLParam(r, "holdID"), }) if err != nil { diff --git a/ee/wallets/pkg/api/handler_holds_void_test.go b/ee/wallets/pkg/api/handler_holds_void_test.go index 99c5b7d283..6e4f8e37be 100644 --- a/ee/wallets/pkg/api/handler_holds_void_test.go +++ b/ee/wallets/pkg/api/handler_holds_void_test.go @@ -45,7 +45,7 @@ func TestHoldsVoid(t *testing.T) { }, }, nil }), - WithCreateTransaction(func(ctx context.Context, name string, script wallet.PostTransaction) (*shared.V2Transaction, error) { + WithCreateTransaction(func(ctx context.Context, name, ik string, script wallet.PostTransaction) (*shared.V2Transaction, error) { compareJSON(t, wallet.PostTransaction{ Script: &shared.V2PostTransactionScript{ Plain: wallet.BuildCancelHoldScript("USD"), diff --git a/ee/wallets/pkg/api/handler_wallets_create_test.go b/ee/wallets/pkg/api/handler_wallets_create_test.go index ff90046e4e..a1d830e37a 100644 --- a/ee/wallets/pkg/api/handler_wallets_create_test.go +++ b/ee/wallets/pkg/api/handler_wallets_create_test.go @@ -33,7 +33,7 @@ func TestWalletsCreate(t *testing.T) { md map[string]string ) testEnv := newTestEnv( - WithAddMetadataToAccount(func(ctx context.Context, l, a string, m map[string]string) error { + WithAddMetadataToAccount(func(ctx context.Context, l, a, ik string, m map[string]string) error { ledger = l account = a md = m diff --git a/ee/wallets/pkg/api/handler_wallets_credit.go b/ee/wallets/pkg/api/handler_wallets_credit.go index 04076fa1c2..8747a9201c 100644 --- a/ee/wallets/pkg/api/handler_wallets_credit.go +++ b/ee/wallets/pkg/api/handler_wallets_credit.go @@ -4,6 +4,8 @@ import ( "errors" "net/http" + "github.com/formancehq/stack/libs/go-libs/api" + wallet "github.com/formancehq/wallets/pkg" "github.com/go-chi/chi/v5" "github.com/go-chi/render" @@ -29,7 +31,7 @@ func (m *MainHandler) creditWalletHandler(w http.ResponseWriter, r *http.Request CreditRequest: *data, } - err := m.manager.Credit(r.Context(), credit) + err := m.manager.Credit(r.Context(), api.IdempotencyKeyFromRequest(r), credit) if err != nil { switch { case errors.Is(err, wallet.ErrBalanceNotExists), diff --git a/ee/wallets/pkg/api/handler_wallets_credit_test.go b/ee/wallets/pkg/api/handler_wallets_credit_test.go index 365ab1ebb0..bbcb2d4edd 100644 --- a/ee/wallets/pkg/api/handler_wallets_credit_test.go +++ b/ee/wallets/pkg/api/handler_wallets_credit_test.go @@ -181,7 +181,7 @@ func TestWalletsCredit(t *testing.T) { postTransaction wallet.PostTransaction ) testEnv = newTestEnv( - WithCreateTransaction(func(ctx context.Context, ledger string, p wallet.PostTransaction) (*shared.V2Transaction, error) { + WithCreateTransaction(func(ctx context.Context, ledger, ik string, p wallet.PostTransaction) (*shared.V2Transaction, error) { require.Equal(t, testEnv.LedgerName(), ledger) postTransaction = p return &testCase.postTransactionResult, nil diff --git a/ee/wallets/pkg/api/handler_wallets_debit.go b/ee/wallets/pkg/api/handler_wallets_debit.go index 4f6ea72d87..f7be696251 100644 --- a/ee/wallets/pkg/api/handler_wallets_debit.go +++ b/ee/wallets/pkg/api/handler_wallets_debit.go @@ -4,6 +4,8 @@ import ( "errors" "net/http" + "github.com/formancehq/stack/libs/go-libs/api" + wallet "github.com/formancehq/wallets/pkg" "github.com/go-chi/chi/v5" "github.com/go-chi/render" @@ -16,7 +18,7 @@ func (m *MainHandler) debitWalletHandler(w http.ResponseWriter, r *http.Request) return } - hold, err := m.manager.Debit(r.Context(), wallet.Debit{ + hold, err := m.manager.Debit(r.Context(), api.IdempotencyKeyFromRequest(r), wallet.Debit{ WalletID: chi.URLParam(r, "walletID"), DebitRequest: *data, }) diff --git a/ee/wallets/pkg/api/handler_wallets_debit_test.go b/ee/wallets/pkg/api/handler_wallets_debit_test.go index 18745cb644..cf4d382abb 100644 --- a/ee/wallets/pkg/api/handler_wallets_debit_test.go +++ b/ee/wallets/pkg/api/handler_wallets_debit_test.go @@ -336,7 +336,7 @@ func TestWalletsDebit(t *testing.T) { }, }, nil }), - WithCreateTransaction(func(ctx context.Context, ledger string, p wallet.PostTransaction) (*shared.V2Transaction, error) { + WithCreateTransaction(func(ctx context.Context, ledger, ik string, p wallet.PostTransaction) (*shared.V2Transaction, error) { require.Equal(t, testEnv.LedgerName(), ledger) postTransaction = p if testCase.postTransactionError != nil { diff --git a/ee/wallets/pkg/api/handler_wallets_patch.go b/ee/wallets/pkg/api/handler_wallets_patch.go index 90eb60d72a..9a667d6085 100644 --- a/ee/wallets/pkg/api/handler_wallets_patch.go +++ b/ee/wallets/pkg/api/handler_wallets_patch.go @@ -4,6 +4,8 @@ import ( "errors" "net/http" + "github.com/formancehq/stack/libs/go-libs/api" + wallet "github.com/formancehq/wallets/pkg" "github.com/go-chi/chi/v5" "github.com/go-chi/render" @@ -16,7 +18,7 @@ func (m *MainHandler) patchWalletHandler(w http.ResponseWriter, r *http.Request) return } - err := m.manager.UpdateWallet(r.Context(), chi.URLParam(r, "walletID"), data) + err := m.manager.UpdateWallet(r.Context(), chi.URLParam(r, "walletID"), api.IdempotencyKeyFromRequest(r), data) if err != nil { switch { case errors.Is(err, wallet.ErrWalletNotFound): diff --git a/ee/wallets/pkg/api/handler_wallets_patch_test.go b/ee/wallets/pkg/api/handler_wallets_patch_test.go index 2fffadd996..c18eec6724 100644 --- a/ee/wallets/pkg/api/handler_wallets_patch_test.go +++ b/ee/wallets/pkg/api/handler_wallets_patch_test.go @@ -42,7 +42,7 @@ func TestWalletsPatch(t *testing.T) { }, }, nil }), - WithAddMetadataToAccount(func(ctx context.Context, ledger, account string, md map[string]string) error { + WithAddMetadataToAccount(func(ctx context.Context, ledger, account, ik string, md map[string]string) error { require.Equal(t, testEnv.LedgerName(), ledger) require.Equal(t, testEnv.Chart().GetMainBalanceAccount(w.ID), account) compareJSON(t, metadata.Metadata{ diff --git a/ee/wallets/pkg/api/utils_test.go b/ee/wallets/pkg/api/utils_test.go index 0a549c2ded..cf166543c5 100644 --- a/ee/wallets/pkg/api/utils_test.go +++ b/ee/wallets/pkg/api/utils_test.go @@ -92,11 +92,11 @@ func newTestEnv(opts ...Option) *testEnv { } type ( - addMetadataToAccountFn func(ctx context.Context, ledger, account string, metadata map[string]string) error + addMetadataToAccountFn func(ctx context.Context, ledger, account, ik string, metadata map[string]string) error getAccountFn func(ctx context.Context, ledger, account string) (*wallet.AccountWithVolumesAndBalances, error) listAccountsFn func(ctx context.Context, ledger string, query wallet.ListAccountsQuery) (*wallet.AccountsCursorResponseCursor, error) listTransactionsFn func(ctx context.Context, ledger string, query wallet.ListTransactionsQuery) (*shared.V2TransactionsCursorResponseCursor, error) - createTransactionFn func(ctx context.Context, ledger string, postTransaction wallet.PostTransaction) (*shared.V2Transaction, error) + createTransactionFn func(ctx context.Context, ledger, ik string, postTransaction wallet.PostTransaction) (*shared.V2Transaction, error) ) type LedgerMock struct { @@ -111,8 +111,8 @@ func (l *LedgerMock) EnsureLedgerExists(ctx context.Context, name string) error return nil } -func (l *LedgerMock) AddMetadataToAccount(ctx context.Context, ledger, account string, metadata map[string]string) error { - return l.addMetadataToAccount(ctx, ledger, account, metadata) +func (l *LedgerMock) AddMetadataToAccount(ctx context.Context, ledger, account, ik string, metadata map[string]string) error { + return l.addMetadataToAccount(ctx, ledger, account, ik, metadata) } func (l *LedgerMock) GetAccount(ctx context.Context, ledger, account string) (*wallet.AccountWithVolumesAndBalances, error) { @@ -123,8 +123,8 @@ func (l *LedgerMock) ListAccounts(ctx context.Context, ledger string, query wall return l.listAccounts(ctx, ledger, query) } -func (l *LedgerMock) CreateTransaction(ctx context.Context, ledger string, postTransaction wallet.PostTransaction) (*shared.V2Transaction, error) { - return l.createTransaction(ctx, ledger, postTransaction) +func (l *LedgerMock) CreateTransaction(ctx context.Context, ledger, ik string, postTransaction wallet.PostTransaction) (*shared.V2Transaction, error) { + return l.createTransaction(ctx, ledger, ik, postTransaction) } func (l *LedgerMock) ListTransactions(ctx context.Context, ledger string, query wallet.ListTransactionsQuery) (*shared.V2TransactionsCursorResponseCursor, error) { diff --git a/ee/wallets/pkg/ledger_interface.go b/ee/wallets/pkg/ledger_interface.go index 0ba9891675..3a0bdd102e 100644 --- a/ee/wallets/pkg/ledger_interface.go +++ b/ee/wallets/pkg/ledger_interface.go @@ -98,11 +98,11 @@ func (c AccountsCursorResponseCursor) GetHasMore() bool { type Ledger interface { EnsureLedgerExists(ctx context.Context, name string) error - AddMetadataToAccount(ctx context.Context, ledger, account string, metadata map[string]string) error + AddMetadataToAccount(ctx context.Context, ledger, account, ik string, metadata map[string]string) error GetAccount(ctx context.Context, ledger, account string) (*AccountWithVolumesAndBalances, error) ListAccounts(ctx context.Context, ledger string, query ListAccountsQuery) (*AccountsCursorResponseCursor, error) ListTransactions(ctx context.Context, ledger string, query ListTransactionsQuery) (*shared.V2TransactionsCursorResponseCursor, error) - CreateTransaction(ctx context.Context, ledger string, postTransaction PostTransaction) (*shared.V2Transaction, error) + CreateTransaction(ctx context.Context, ledger, ik string, postTransaction PostTransaction) (*shared.V2Transaction, error) } type DefaultLedger struct { @@ -179,7 +179,7 @@ func (d DefaultLedger) ListTransactions(ctx context.Context, ledger string, q Li return &rsp.V2TransactionsCursorResponse.Cursor, nil } -func (d DefaultLedger) CreateTransaction(ctx context.Context, ledger string, transaction PostTransaction) (*shared.V2Transaction, error) { +func (d DefaultLedger) CreateTransaction(ctx context.Context, ledger, ik string, transaction PostTransaction) (*shared.V2Transaction, error) { ret, err := d.client.Ledger.V2CreateTransaction(ctx, operations.V2CreateTransactionRequest{ V2PostTransaction: shared.V2PostTransaction{ Metadata: transaction.Metadata, @@ -193,7 +193,8 @@ func (d DefaultLedger) CreateTransaction(ctx context.Context, ledger string, tra return &transaction.Timestamp.Time }(), }, - Ledger: ledger, + Ledger: ledger, + IdempotencyKey: pointer.For(ik), }) if err != nil { return nil, err @@ -202,12 +203,13 @@ func (d DefaultLedger) CreateTransaction(ctx context.Context, ledger string, tra return &ret.V2CreateTransactionResponse.Data, nil } -func (d DefaultLedger) AddMetadataToAccount(ctx context.Context, ledger, account string, metadata map[string]string) error { +func (d DefaultLedger) AddMetadataToAccount(ctx context.Context, ledger, account, ik string, metadata map[string]string) error { _, err := d.client.Ledger.V2AddMetadataToAccount(ctx, operations.V2AddMetadataToAccountRequest{ - RequestBody: metadata, - Address: account, - Ledger: ledger, + RequestBody: metadata, + Address: account, + Ledger: ledger, + IdempotencyKey: pointer.For(ik), }) if err != nil { return err diff --git a/ee/wallets/pkg/manager.go b/ee/wallets/pkg/manager.go index f9106b381e..2fcd66a90a 100644 --- a/ee/wallets/pkg/manager.go +++ b/ee/wallets/pkg/manager.go @@ -106,7 +106,7 @@ func (m *Manager) Init(ctx context.Context) error { } //nolint:cyclop -func (m *Manager) Debit(ctx context.Context, debit Debit) (*DebitHold, error) { +func (m *Manager) Debit(ctx context.Context, ik string, debit Debit) (*DebitHold, error) { if err := debit.Validate(); err != nil { return nil, err } @@ -181,14 +181,14 @@ func (m *Manager) Debit(ctx context.Context, debit Debit) (*DebitHold, error) { postTransaction.Reference = &debit.Reference } - if err := m.CreateTransaction(ctx, postTransaction); err != nil { + if err := m.CreateTransaction(ctx, ik, postTransaction); err != nil { return nil, err } return hold, nil } -func (m *Manager) ConfirmHold(ctx context.Context, debit ConfirmHold) error { +func (m *Manager) ConfirmHold(ctx context.Context, ik string, debit ConfirmHold) error { account, err := m.client.GetAccount(ctx, m.ledgerName, m.chart.GetHoldAccount(debit.HoldID)) if err != nil { return errors.Wrap(err, "getting account") @@ -227,14 +227,14 @@ func (m *Manager) ConfirmHold(ctx context.Context, debit ConfirmHold) error { Metadata: TransactionMetadata(metadata.Metadata{}), } - if err := m.CreateTransaction(ctx, postTransaction); err != nil { + if err := m.CreateTransaction(ctx, ik, postTransaction); err != nil { return err } return nil } -func (m *Manager) VoidHold(ctx context.Context, void VoidHold) error { +func (m *Manager) VoidHold(ctx context.Context, ik string, void VoidHold) error { account, err := m.client.GetAccount(ctx, m.ledgerName, m.chart.GetHoldAccount(void.HoldID)) if err != nil { return errors.Wrap(err, "getting account") @@ -256,14 +256,14 @@ func (m *Manager) VoidHold(ctx context.Context, void VoidHold) error { Metadata: TransactionMetadata(metadata.Metadata{}), } - if err := m.CreateTransaction(ctx, postTransaction); err != nil { + if err := m.CreateTransaction(ctx, ik, postTransaction); err != nil { return err } return nil } -func (m *Manager) Credit(ctx context.Context, credit Credit) error { +func (m *Manager) Credit(ctx context.Context, ik string, credit Credit) error { if err := credit.Validate(); err != nil { return err } @@ -293,15 +293,15 @@ func (m *Manager) Credit(ctx context.Context, credit Credit) error { postTransaction.Reference = &credit.Reference } - if err := m.CreateTransaction(ctx, postTransaction); err != nil { + if err := m.CreateTransaction(ctx, ik, postTransaction); err != nil { return err } return nil } -func (m *Manager) CreateTransaction(ctx context.Context, postTransaction PostTransaction) error { - if _, err := m.client.CreateTransaction(ctx, m.ledgerName, postTransaction); err != nil { +func (m *Manager) CreateTransaction(ctx context.Context, ik string, postTransaction PostTransaction) error { + if _, err := m.client.CreateTransaction(ctx, m.ledgerName, ik, postTransaction); err != nil { switch err := err.(type) { case *sdkerrors.WalletsErrorResponse: if err.ErrorCode == sdkerrors.SchemasWalletsErrorResponseErrorCodeInsufficientFund { @@ -417,6 +417,7 @@ func (m *Manager) CreateWallet(ctx context.Context, data *CreateRequest) (*Walle ctx, m.ledgerName, m.chart.GetMainBalanceAccount(wallet.ID), + "", wallet.LedgerMetadata(), ); err != nil { return nil, errors.Wrap(err, "adding metadata to account") @@ -425,7 +426,7 @@ func (m *Manager) CreateWallet(ctx context.Context, data *CreateRequest) (*Walle return &wallet, nil } -func (m *Manager) UpdateWallet(ctx context.Context, id string, data *PatchRequest) error { +func (m *Manager) UpdateWallet(ctx context.Context, id, ik string, data *PatchRequest) error { account, err := m.client.GetAccount(ctx, m.ledgerName, m.chart.GetMainBalanceAccount(id)) if err != nil { return ErrWalletNotFound @@ -442,7 +443,7 @@ func (m *Manager) UpdateWallet(ctx context.Context, id string, data *PatchReques meta := metadata.Metadata(account.GetMetadata()) meta = meta.Merge(EncodeCustomMetadata(newCustomMetadata)) - if err := m.client.AddMetadataToAccount(ctx, m.ledgerName, m.chart.GetMainBalanceAccount(id), meta); err != nil { + if err := m.client.AddMetadataToAccount(ctx, m.ledgerName, m.chart.GetMainBalanceAccount(id), ik, meta); err != nil { return errors.Wrap(err, "adding metadata to account") } @@ -584,6 +585,7 @@ func (m *Manager) CreateBalance(ctx context.Context, data *CreateBalance) (*Bala ctx, m.ledgerName, m.chart.GetBalanceAccount(data.WalletID, balance.Name), + "", balance.LedgerMetadata(data.WalletID), ); err != nil { return nil, errors.Wrap(err, "adding metadata to account") diff --git a/libs/go-libs/api/idempotency.go b/libs/go-libs/api/idempotency.go new file mode 100644 index 0000000000..c2f58c05dc --- /dev/null +++ b/libs/go-libs/api/idempotency.go @@ -0,0 +1,7 @@ +package api + +import "net/http" + +func IdempotencyKeyFromRequest(r *http.Request) string { + return r.Header.Get("Idempotency-Key") +} diff --git a/releases/sdks/go/.speakeasy/gen.lock b/releases/sdks/go/.speakeasy/gen.lock index f635d35c6d..c668ea4903 100755 --- a/releases/sdks/go/.speakeasy/gen.lock +++ b/releases/sdks/go/.speakeasy/gen.lock @@ -1,7 +1,7 @@ lockVersion: 2.0.0 id: 7eac0a45-60a2-40bb-9e85-26bd77ec2a6d management: - docChecksum: 09fd9d887717d67e69a8602402ba2302 + docChecksum: 9b5e414195f0c0428e44134300a0532d docVersion: v0.0.0 speakeasyVersion: 1.292.0 generationVersion: 2.332.4 diff --git a/releases/sdks/go/docs/pkg/models/operations/confirmholdrequest.md b/releases/sdks/go/docs/pkg/models/operations/confirmholdrequest.md index ab8b18c1fd..32876ade63 100644 --- a/releases/sdks/go/docs/pkg/models/operations/confirmholdrequest.md +++ b/releases/sdks/go/docs/pkg/models/operations/confirmholdrequest.md @@ -6,4 +6,5 @@ | Field | Type | Required | Description | | ------------------------------------------------------------------------------ | ------------------------------------------------------------------------------ | ------------------------------------------------------------------------------ | ------------------------------------------------------------------------------ | | `ConfirmHoldRequest` | [*shared.ConfirmHoldRequest](../../../pkg/models/shared/confirmholdrequest.md) | :heavy_minus_sign: | N/A | +| `IdempotencyKey` | **string* | :heavy_minus_sign: | Use an idempotency key | | `HoldID` | *string* | :heavy_check_mark: | N/A | \ No newline at end of file diff --git a/releases/sdks/go/docs/pkg/models/operations/creditwalletrequest.md b/releases/sdks/go/docs/pkg/models/operations/creditwalletrequest.md index a399e2b51c..3d1aa9e822 100644 --- a/releases/sdks/go/docs/pkg/models/operations/creditwalletrequest.md +++ b/releases/sdks/go/docs/pkg/models/operations/creditwalletrequest.md @@ -6,4 +6,5 @@ | Field | Type | Required | Description | Example | | ------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------- | | `CreditWalletRequest` | [*shared.CreditWalletRequest](../../../pkg/models/shared/creditwalletrequest.md) | :heavy_minus_sign: | N/A | {
"amount": {
"asset": "USD/2",
"amount": 100
},
"metadata": {
"key": ""
},
"sources": []
} | +| `IdempotencyKey` | **string* | :heavy_minus_sign: | Use an idempotency key | | | `ID` | *string* | :heavy_check_mark: | N/A | | \ No newline at end of file diff --git a/releases/sdks/go/docs/pkg/models/operations/debitwalletrequest.md b/releases/sdks/go/docs/pkg/models/operations/debitwalletrequest.md index 714425cb7c..3dd98e2fb9 100644 --- a/releases/sdks/go/docs/pkg/models/operations/debitwalletrequest.md +++ b/releases/sdks/go/docs/pkg/models/operations/debitwalletrequest.md @@ -6,4 +6,5 @@ | Field | Type | Required | Description | Example | | --------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------- | | `DebitWalletRequest` | [*shared.DebitWalletRequest](../../../pkg/models/shared/debitwalletrequest.md) | :heavy_minus_sign: | N/A | {
"amount": {
"asset": "USD/2",
"amount": 100
},
"metadata": {
"key": ""
},
"pending": true
} | +| `IdempotencyKey` | **string* | :heavy_minus_sign: | Use an idempotency key | | | `ID` | *string* | :heavy_check_mark: | N/A | | \ No newline at end of file diff --git a/releases/sdks/go/docs/pkg/models/operations/updatewalletrequest.md b/releases/sdks/go/docs/pkg/models/operations/updatewalletrequest.md index 5186bdb6a1..5970671d68 100644 --- a/releases/sdks/go/docs/pkg/models/operations/updatewalletrequest.md +++ b/releases/sdks/go/docs/pkg/models/operations/updatewalletrequest.md @@ -5,5 +5,6 @@ | Field | Type | Required | Description | | ------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------ | +| `IdempotencyKey` | **string* | :heavy_minus_sign: | Use an idempotency key | | `RequestBody` | [*operations.UpdateWalletRequestBody](../../../pkg/models/operations/updatewalletrequestbody.md) | :heavy_minus_sign: | N/A | | `ID` | *string* | :heavy_check_mark: | N/A | \ No newline at end of file diff --git a/releases/sdks/go/docs/pkg/models/operations/voidholdrequest.md b/releases/sdks/go/docs/pkg/models/operations/voidholdrequest.md index 48a62532af..b2ce4b0542 100644 --- a/releases/sdks/go/docs/pkg/models/operations/voidholdrequest.md +++ b/releases/sdks/go/docs/pkg/models/operations/voidholdrequest.md @@ -3,6 +3,7 @@ ## Fields -| Field | Type | Required | Description | -| ------------------ | ------------------ | ------------------ | ------------------ | -| `HoldID` | *string* | :heavy_check_mark: | N/A | \ No newline at end of file +| Field | Type | Required | Description | +| ---------------------- | ---------------------- | ---------------------- | ---------------------- | +| `IdempotencyKey` | **string* | :heavy_minus_sign: | Use an idempotency key | +| `HoldID` | *string* | :heavy_check_mark: | N/A | \ No newline at end of file diff --git a/releases/sdks/go/pkg/models/operations/confirmhold.go b/releases/sdks/go/pkg/models/operations/confirmhold.go index c8be47d8ae..09096dfa92 100644 --- a/releases/sdks/go/pkg/models/operations/confirmhold.go +++ b/releases/sdks/go/pkg/models/operations/confirmhold.go @@ -9,7 +9,9 @@ import ( type ConfirmHoldRequest struct { ConfirmHoldRequest *shared.ConfirmHoldRequest `request:"mediaType=application/json"` - HoldID string `pathParam:"style=simple,explode=false,name=hold_id"` + // Use an idempotency key + IdempotencyKey *string `header:"style=simple,explode=false,name=Idempotency-Key"` + HoldID string `pathParam:"style=simple,explode=false,name=hold_id"` } func (o *ConfirmHoldRequest) GetConfirmHoldRequest() *shared.ConfirmHoldRequest { @@ -19,6 +21,13 @@ func (o *ConfirmHoldRequest) GetConfirmHoldRequest() *shared.ConfirmHoldRequest return o.ConfirmHoldRequest } +func (o *ConfirmHoldRequest) GetIdempotencyKey() *string { + if o == nil { + return nil + } + return o.IdempotencyKey +} + func (o *ConfirmHoldRequest) GetHoldID() string { if o == nil { return "" diff --git a/releases/sdks/go/pkg/models/operations/creditwallet.go b/releases/sdks/go/pkg/models/operations/creditwallet.go index eef13532f8..8757b3a088 100644 --- a/releases/sdks/go/pkg/models/operations/creditwallet.go +++ b/releases/sdks/go/pkg/models/operations/creditwallet.go @@ -9,7 +9,9 @@ import ( type CreditWalletRequest struct { CreditWalletRequest *shared.CreditWalletRequest `request:"mediaType=application/json"` - ID string `pathParam:"style=simple,explode=false,name=id"` + // Use an idempotency key + IdempotencyKey *string `header:"style=simple,explode=false,name=Idempotency-Key"` + ID string `pathParam:"style=simple,explode=false,name=id"` } func (o *CreditWalletRequest) GetCreditWalletRequest() *shared.CreditWalletRequest { @@ -19,6 +21,13 @@ func (o *CreditWalletRequest) GetCreditWalletRequest() *shared.CreditWalletReque return o.CreditWalletRequest } +func (o *CreditWalletRequest) GetIdempotencyKey() *string { + if o == nil { + return nil + } + return o.IdempotencyKey +} + func (o *CreditWalletRequest) GetID() string { if o == nil { return "" diff --git a/releases/sdks/go/pkg/models/operations/debitwallet.go b/releases/sdks/go/pkg/models/operations/debitwallet.go index 0d7f7101c0..5d06c65a79 100644 --- a/releases/sdks/go/pkg/models/operations/debitwallet.go +++ b/releases/sdks/go/pkg/models/operations/debitwallet.go @@ -9,7 +9,9 @@ import ( type DebitWalletRequest struct { DebitWalletRequest *shared.DebitWalletRequest `request:"mediaType=application/json"` - ID string `pathParam:"style=simple,explode=false,name=id"` + // Use an idempotency key + IdempotencyKey *string `header:"style=simple,explode=false,name=Idempotency-Key"` + ID string `pathParam:"style=simple,explode=false,name=id"` } func (o *DebitWalletRequest) GetDebitWalletRequest() *shared.DebitWalletRequest { @@ -19,6 +21,13 @@ func (o *DebitWalletRequest) GetDebitWalletRequest() *shared.DebitWalletRequest return o.DebitWalletRequest } +func (o *DebitWalletRequest) GetIdempotencyKey() *string { + if o == nil { + return nil + } + return o.IdempotencyKey +} + func (o *DebitWalletRequest) GetID() string { if o == nil { return "" diff --git a/releases/sdks/go/pkg/models/operations/updatewallet.go b/releases/sdks/go/pkg/models/operations/updatewallet.go index 408313dce5..a526db51e2 100644 --- a/releases/sdks/go/pkg/models/operations/updatewallet.go +++ b/releases/sdks/go/pkg/models/operations/updatewallet.go @@ -19,8 +19,17 @@ func (o *UpdateWalletRequestBody) GetMetadata() map[string]string { } type UpdateWalletRequest struct { - RequestBody *UpdateWalletRequestBody `request:"mediaType=application/json"` - ID string `pathParam:"style=simple,explode=false,name=id"` + // Use an idempotency key + IdempotencyKey *string `header:"style=simple,explode=false,name=Idempotency-Key"` + RequestBody *UpdateWalletRequestBody `request:"mediaType=application/json"` + ID string `pathParam:"style=simple,explode=false,name=id"` +} + +func (o *UpdateWalletRequest) GetIdempotencyKey() *string { + if o == nil { + return nil + } + return o.IdempotencyKey } func (o *UpdateWalletRequest) GetRequestBody() *UpdateWalletRequestBody { diff --git a/releases/sdks/go/pkg/models/operations/voidhold.go b/releases/sdks/go/pkg/models/operations/voidhold.go index fed071ee44..e1a717ac29 100644 --- a/releases/sdks/go/pkg/models/operations/voidhold.go +++ b/releases/sdks/go/pkg/models/operations/voidhold.go @@ -7,7 +7,16 @@ import ( ) type VoidHoldRequest struct { - HoldID string `pathParam:"style=simple,explode=false,name=hold_id"` + // Use an idempotency key + IdempotencyKey *string `header:"style=simple,explode=false,name=Idempotency-Key"` + HoldID string `pathParam:"style=simple,explode=false,name=hold_id"` +} + +func (o *VoidHoldRequest) GetIdempotencyKey() *string { + if o == nil { + return nil + } + return o.IdempotencyKey } func (o *VoidHoldRequest) GetHoldID() string { diff --git a/releases/sdks/go/wallets.go b/releases/sdks/go/wallets.go index 72a7bf1476..585f4f7bb7 100644 --- a/releases/sdks/go/wallets.go +++ b/releases/sdks/go/wallets.go @@ -54,6 +54,8 @@ func (s *Wallets) ConfirmHold(ctx context.Context, request operations.ConfirmHol req.Header.Set("User-Agent", s.sdkConfiguration.UserAgent) req.Header.Set("Content-Type", reqContentType) + utils.PopulateHeaders(ctx, req, request, nil) + if err := utils.PopulateSecurity(ctx, req, s.sdkConfiguration.Security); err != nil { return nil, err } @@ -355,6 +357,8 @@ func (s *Wallets) CreditWallet(ctx context.Context, request operations.CreditWal req.Header.Set("User-Agent", s.sdkConfiguration.UserAgent) req.Header.Set("Content-Type", reqContentType) + utils.PopulateHeaders(ctx, req, request, nil) + if err := utils.PopulateSecurity(ctx, req, s.sdkConfiguration.Security); err != nil { return nil, err } @@ -448,6 +452,8 @@ func (s *Wallets) DebitWallet(ctx context.Context, request operations.DebitWalle req.Header.Set("User-Agent", s.sdkConfiguration.UserAgent) req.Header.Set("Content-Type", reqContentType) + utils.PopulateHeaders(ctx, req, request, nil) + if err := utils.PopulateSecurity(ctx, req, s.sdkConfiguration.Security); err != nil { return nil, err } @@ -1340,6 +1346,8 @@ func (s *Wallets) UpdateWallet(ctx context.Context, request operations.UpdateWal req.Header.Set("User-Agent", s.sdkConfiguration.UserAgent) req.Header.Set("Content-Type", reqContentType) + utils.PopulateHeaders(ctx, req, request, nil) + if err := utils.PopulateSecurity(ctx, req, s.sdkConfiguration.Security); err != nil { return nil, err } @@ -1427,6 +1435,8 @@ func (s *Wallets) VoidHold(ctx context.Context, request operations.VoidHoldReque req.Header.Set("Accept", "application/json") req.Header.Set("User-Agent", s.sdkConfiguration.UserAgent) + utils.PopulateHeaders(ctx, req, request, nil) + if err := utils.PopulateSecurity(ctx, req, s.sdkConfiguration.Security); err != nil { return nil, err } diff --git a/tests/integration/suite/wallets-credit.go b/tests/integration/suite/wallets-credit.go index 6a47c5dc00..18ba9de08b 100644 --- a/tests/integration/suite/wallets-credit.go +++ b/tests/integration/suite/wallets-credit.go @@ -4,6 +4,7 @@ import ( "github.com/formancehq/formance-sdk-go/v2/pkg/models/operations" "github.com/formancehq/formance-sdk-go/v2/pkg/models/sdkerrors" "github.com/formancehq/formance-sdk-go/v2/pkg/models/shared" + "github.com/formancehq/stack/libs/go-libs/pointer" . "github.com/formancehq/stack/tests/integration/internal" "github.com/formancehq/stack/tests/integration/internal/modules" "github.com/google/uuid" @@ -44,10 +45,36 @@ var _ = WithModules([]*Module{modules.Auth, modules.Ledger, modules.Wallets}, fu Metadata: map[string]string{}, }, ID: response.CreateWalletResponse.Data.ID, + IdempotencyKey: pointer.For("foo"), }) Expect(err).To(Succeed()) }) It("should be ok", func() {}) + Then("crediting again with the same ik", func() { + BeforeEach(func() { + _, err := Client().Wallets.CreditWallet(TestContext(), operations.CreditWalletRequest{ + CreditWalletRequest: &shared.CreditWalletRequest{ + Amount: shared.Monetary{ + Amount: big.NewInt(1000), + Asset: "USD/2", + }, + Sources: []shared.Subject{}, + Metadata: map[string]string{}, + }, + ID: response.CreateWalletResponse.Data.ID, + IdempotencyKey: pointer.For("foo"), + }) + Expect(err).To(Succeed()) + }) + It("Should not trigger any movements", func() { + balance, err := Client().Wallets.GetBalance(TestContext(), operations.GetBalanceRequest{ + BalanceName: "main", + ID: response.CreateWalletResponse.Data.ID, + }) + Expect(err).To(Succeed()) + Expect(balance.GetBalanceResponse.Data.Assets["USD/2"]).To(Equal(big.NewInt(1000))) + }) + }) }) Then("crediting it with specified timestamp", func() { now := time.Now().Round(time.Microsecond).UTC()