diff --git a/docker-compose.yml b/docker-compose.yml index 3aece86d..7b91697d 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,4 +1,4 @@ -version: '3.8' +version: "3.8" volumes: postgres: @@ -10,7 +10,7 @@ services: ports: - 8092:80 environment: - DEBUG: true + DEBUG: true postgres: image: "postgres:14-alpine" @@ -82,7 +82,7 @@ services: image: golang:1.23.4-alpine command: go run ./ server healthcheck: - test: [ "CMD", "curl", "-f", "http://127.0.0.1:8080/_healthcheck" ] + test: ["CMD", "curl", "-f", "http://127.0.0.1:8080/_healthcheck"] interval: 10s timeout: 5s retries: 5 @@ -105,13 +105,13 @@ services: PLUGIN_MAGIC_COOKIE: mysupercookie TEMPORAL_INIT_SEARCH_ATTRIBUTES: true STACK_URL: http://gateway:8092 - STACK_PUBLIC_URL: ${STACK_PUBLIC_URL:?mandatory} + STACK_PUBLIC_URL: http://localhost:8080 payments-worker: image: golang:1.23.4-alpine command: go run ./ worker healthcheck: - test: [ "CMD", "curl", "-f", "http://127.0.0.1:8080/_healthcheck" ] + test: ["CMD", "curl", "-f", "http://127.0.0.1:8080/_healthcheck"] interval: 10s timeout: 5s retries: 5 @@ -133,4 +133,4 @@ services: PLUGIN_MAGIC_COOKIE: mysupercookie TEMPORAL_INIT_SEARCH_ATTRIBUTES: false STACK_URL: http://gateway:8092 - STACK_PUBLIC_URL: ${STACK_PUBLIC_URL:?mandatory} + STACK_PUBLIC_URL: http://localhost:8080 diff --git a/docs/api/README.md b/docs/api/README.md index 8f92dbaf..97017336 100644 --- a/docs/api/README.md +++ b/docs/api/README.md @@ -5730,6 +5730,12 @@ xor xor +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|*anonymous*|[V3CheckoutConfig](#schemav3checkoutconfig)|false|none|none| + +xor + |Name|Type|Required|Restrictions|Description| |---|---|---|---|---| |*anonymous*|[V3ColumnConfig](#schemav3columnconfig)|false|none|none| @@ -5912,6 +5918,42 @@ xor |userCertificateKey|string|true|none|none| |username|string|true|none|none| +

V3CheckoutConfig

+ + + + + + +```json +{ + "entityId": "string", + "environment": "string", + "name": "string", + "oauthClientID": "string", + "oauthClientSecret": "string", + "pageSize": "25", + "pollingPeriod": "2m", + "processingChannelId": "string", + "provider": "Checkout" +} + +``` + +### Properties + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|entityId|string|true|none|none| +|environment|string|true|none|none| +|name|string|true|none|none| +|oauthClientID|string|true|none|none| +|oauthClientSecret|string|true|none|none| +|pageSize|integer|false|none|none| +|pollingPeriod|string|false|none|none| +|processingChannelId|string|true|none|none| +|provider|string|false|none|none| +

V3ColumnConfig

diff --git a/go.mod b/go.mod index 6c1361a7..bc42e99d 100644 --- a/go.mod +++ b/go.mod @@ -12,6 +12,7 @@ require ( github.com/ThreeDotsLabs/watermill v1.4.7 github.com/adyen/adyen-go-api-library/v7 v7.3.1 github.com/bombsimon/logrusr/v3 v3.1.0 + github.com/checkout/checkout-sdk-go v1.7.2 github.com/emvi/iso-639-1 v1.1.1 github.com/formancehq/go-libs/v3 v3.0.2-0.20250814071617-0f5bb98d939b github.com/formancehq/payments/genericclient v0.0.0-00010101000000-000000000000 @@ -132,6 +133,7 @@ require ( github.com/goccy/go-json v0.10.3 // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/golang/snappy v0.0.4 // indirect + github.com/google/go-querystring v1.1.0 // indirect github.com/google/go-tpm v0.9.5 // indirect github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 // indirect github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect diff --git a/go.sum b/go.sum index da4ffb2c..fa509a4f 100644 --- a/go.sum +++ b/go.sum @@ -693,6 +693,8 @@ github.com/census-instrumentation/opencensus-proto v0.4.1/go.mod h1:4T9NM4+4Vw91 github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/checkout/checkout-sdk-go v1.7.2 h1:tRxQ4bT0zxfV1Pox+x5pvAC55Il+BJIQBB1hcuA9HSw= +github.com/checkout/checkout-sdk-go v1.7.2/go.mod h1:NL0iaELZA1BleG4dUINHaa8LgcizKUnzWWuTVukTswo= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= @@ -776,6 +778,7 @@ github.com/formancehq/go-libs/v3 v3.0.2-0.20250814071617-0f5bb98d939b h1:jmWZvgO github.com/formancehq/go-libs/v3 v3.0.2-0.20250814071617-0f5bb98d939b/go.mod h1:HgSiCLTW/yu3Or5gWB20/N8/xWV6zkdI8WoQXP4vLLs= github.com/fortytw2/leaktest v1.3.0 h1:u8491cBMTQ8ft8aeV+adlcytMZylmA5nnwwkRZjI8vw= github.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g= +github.com/gabriel-vasile/mimetype v1.4.1/go.mod h1:05Vi0w3Y9c/lNvJOdmIwvrrAhX3rYhfQQCaf9VJcv7M= github.com/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM= github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8= github.com/get-momo/atlar-v1-go-client v1.4.0 h1:DIRwP3gRfvdXAxEeMNua6HPsScXZzDB9nySdYVv5FD0= @@ -915,6 +918,8 @@ github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeN github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= +github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= github.com/google/go-tpm v0.9.5 h1:ocUmnDebX54dnW+MQWGQRbdaAcJELsa6PqZhJ48KwVU= github.com/google/go-tpm v0.9.5/go.mod h1:h9jEsEECg7gtLis0upRBQU+GhYVH6jMjrFxI8u6bVUY= github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= diff --git a/internal/connectors/plugins/public/checkout/accounts.go b/internal/connectors/plugins/public/checkout/accounts.go new file mode 100644 index 00000000..f2230037 --- /dev/null +++ b/internal/connectors/plugins/public/checkout/accounts.go @@ -0,0 +1,58 @@ +package checkout + +import ( + "context" + "encoding/json" + "time" + + "github.com/formancehq/payments/internal/models" +) + +type accountsState struct { + LastPage int `json:"lastPage"` +} + +func (p *Plugin) fetchNextAccounts(ctx context.Context, req models.FetchNextAccountsRequest) (models.FetchNextAccountsResponse, error) { + const page = 0 + + pagedAccounts, err := p.client.GetAccounts(ctx, page, req.PageSize) + if err != nil { + return models.FetchNextAccountsResponse{}, err + } + + accounts := make([]models.PSPAccount, 0, len(pagedAccounts)) + for _, acc := range pagedAccounts { + raw, _ := json.Marshal(acc) + + md := map[string]string{ + "status": acc.Status, + } + + var namePtr *string + if acc.Name != "" { + n := acc.Name + namePtr = &n + } + + accounts = append(accounts, models.PSPAccount{ + Reference: acc.ID, + Name: namePtr, + DefaultAsset: nil, + Metadata: md, + Raw: raw, + CreatedAt: time.Now().UTC(), + }) + } + + if req.PageSize > 0 && len(accounts) > req.PageSize { + accounts = accounts[:req.PageSize] + } + + newState, _ := json.Marshal(accountsState{LastPage: 0}) + + return models.FetchNextAccountsResponse{ + Accounts: accounts, + NewState: newState, + HasMore: false, + }, nil +} diff --git a/internal/connectors/plugins/public/checkout/balances.go b/internal/connectors/plugins/public/checkout/balances.go new file mode 100644 index 00000000..cae0208f --- /dev/null +++ b/internal/connectors/plugins/public/checkout/balances.go @@ -0,0 +1,35 @@ +package checkout + +import ( + "context" + "math/big" + "time" + + "github.com/formancehq/payments/internal/models" + "github.com/formancehq/go-libs/v3/currency" +) + +func (p *Plugin) fetchNextBalances(ctx context.Context, req models.FetchNextBalancesRequest) (models.FetchNextBalancesResponse, error) { + balances, err := p.client.GetAccountBalances(ctx) + if err != nil { + return models.FetchNextBalancesResponse{}, err + } + + accountBalances := make([]models.PSPBalance, 0, len(balances)) + + for _, b := range balances { + asset := currency.FormatAsset(supportedCurrenciesWithDecimal, b.Currency) + + accountBalances = append(accountBalances, models.PSPBalance{ + AccountReference: b.CurrencyAccountID, + Asset: asset, + Amount: big.NewInt(b.Available), + CreatedAt: time.Now().UTC(), + }) + } + + return models.FetchNextBalancesResponse{ + Balances: accountBalances, + HasMore: false, + }, nil +} diff --git a/internal/connectors/plugins/public/checkout/capabilities.go b/internal/connectors/plugins/public/checkout/capabilities.go new file mode 100644 index 00000000..5069899a --- /dev/null +++ b/internal/connectors/plugins/public/checkout/capabilities.go @@ -0,0 +1,12 @@ +package checkout + +import "github.com/formancehq/payments/internal/models" + +var capabilities = []models.Capability{ + models.CAPABILITY_FETCH_ACCOUNTS, + models.CAPABILITY_FETCH_BALANCES, + models.CAPABILITY_FETCH_PAYMENTS, + + models.CAPABILITY_CREATE_TRANSFER, + models.CAPABILITY_CREATE_PAYOUT, +} diff --git a/internal/connectors/plugins/public/checkout/client/accounts.go b/internal/connectors/plugins/public/checkout/client/accounts.go new file mode 100644 index 00000000..9c390db9 --- /dev/null +++ b/internal/connectors/plugins/public/checkout/client/accounts.go @@ -0,0 +1,51 @@ +package client + +import ( + "context" + "encoding/json" + "fmt" + + "github.com/formancehq/payments/internal/connectors/metrics" +) + +type Account struct { + ID string `json:"id"` + Name string `json:"name"` + Status string `json:"status"` +} + +func (c *client) GetAccounts(ctx context.Context, page int, pageSize int) ([]*Account, error) { + ctx = context.WithValue(ctx, metrics.MetricOperationContextKey, "list_accounts") + + if page > 1 { + return []*Account{}, nil + } + if c.sdk == nil || c.entityID == "" { + return nil, fmt.Errorf("checkout sdk not initialized or missing entityID") + } + + entity, err := c.sdk.Accounts.GetEntity(c.entityID) + if err != nil { + return nil, fmt.Errorf("checkout.accounts.getEntity(%s): %w", c.entityID, err) + } + + if b, err := json.MarshalIndent(entity, "", " "); err == nil { + fmt.Printf("Received entity from Checkout: %s\n", string(b)) + } + + id := c.entityID + name := fmt.Sprint(entity.Company.LegalName) + status := fmt.Sprint(entity.Status) + + accounts := []*Account{{ + ID: id, + Name: name, + Status: status, + }} + + if b, _ := json.Marshal(accounts); true { + fmt.Printf("[checkout] GetAccounts returns %d account(s): %s\n", len(accounts), string(b)) + } + + return accounts, nil +} diff --git a/internal/connectors/plugins/public/checkout/client/balances.go b/internal/connectors/plugins/public/checkout/client/balances.go new file mode 100644 index 00000000..446a4830 --- /dev/null +++ b/internal/connectors/plugins/public/checkout/client/balances.go @@ -0,0 +1,56 @@ +package client + +import ( + "context" + "encoding/json" + "fmt" + + "github.com/checkout/checkout-sdk-go/balances" + + "github.com/formancehq/payments/internal/connectors/metrics" +) + +type Balance struct { + Descriptor string `json:"descriptor"` + CurrencyAccountID string `json:"currencyAccountId"` + Currency string `json:"currency"` + Available int64 `json:"available"` + Pending int64 `json:"pending"` + Payable int64 `json:"payable"` + Collateral int64 `json:"collateral"` +} + +func (c *client) GetAccountBalances(ctx context.Context) ([]*Balance, error) { + ctx = context.WithValue(ctx, metrics.MetricOperationContextKey, "list_account_balances") + + if c.sdk == nil || c.entityID == "" { + return nil, fmt.Errorf("checkout sdk not initialized or missing entityID") + } + + resp, err := c.sdk.Balances.RetrieveEntityBalances( + c.entityID, + balances.QueryFilter{WithCurrencyAccountId: true}, + ) + if err != nil { + return nil, fmt.Errorf("checkout.accounts.getEntityBalances(%s): %w", c.entityID, err) + } + + balances := make([]*Balance, 0, len(resp.Data)) + for _, ab := range resp.Data { + balances = append(balances, &Balance{ + Descriptor: ab.Descriptor, + CurrencyAccountID: ab.CurrencyAccountId, + Currency: ab.HoldingCurrency, + Available: ab.Balances.Available, + Pending: ab.Balances.Pending, + Payable: ab.Balances.Payable, + Collateral: ab.Balances.Collateral, + }) + } + + if b, _ := json.Marshal(balances); true { + fmt.Printf("[checkout] GetAccountBalances returns %d balance(s): %s\n", len(balances), string(b)) + } + + return balances, nil +} diff --git a/internal/connectors/plugins/public/checkout/client/client.go b/internal/connectors/plugins/public/checkout/client/client.go new file mode 100644 index 00000000..022b9c85 --- /dev/null +++ b/internal/connectors/plugins/public/checkout/client/client.go @@ -0,0 +1,141 @@ +package client + +import ( + "context" + "encoding/json" + "fmt" + "net/http" + "net/url" + "strings" + "time" + + "github.com/checkout/checkout-sdk-go" + "github.com/checkout/checkout-sdk-go/configuration" + "github.com/checkout/checkout-sdk-go/nas" + + "github.com/formancehq/payments/internal/connectors/metrics" +) + +type Client interface { + GetAccounts(ctx context.Context, page int, pageSize int) ([]*Account, error) + GetAccountBalances(ctx context.Context) ([]*Balance, error) + GetExternalAccounts(ctx context.Context, page int, pageSize int) ([]*ExternalAccount, error) + GetTransactions(ctx context.Context, page, pageSize int) ([]*Transaction, error) + InitiateTransfer(ctx context.Context, tr *TransferRequest) (*TransferResponse, error) + InitiatePayout(ctx context.Context, pr *PayoutRequest) (*PayoutResponse, error) +} + +type client struct { + sdk *nas.Api + httpClient *http.Client + apiBase string + apiAuthUrl string + oauthClientID string + oauthClientSecret string + entityID string + processingChannelId string +} + +type acceptHeaderTransport struct { + base http.RoundTripper +} + +func (t *acceptHeaderTransport) RoundTrip(req *http.Request) (*http.Response, error) { + r := req.Clone(req.Context()) + r.Header.Set("Accept", "application/json; schema_version=3.0") + if r.Header.Get("Content-Type") == "" { + r.Header.Set("Content-Type", "application/json") + } + return t.base.RoundTrip(r) +} + +func New( + env string, + oauthClientID string, + oauthClientSecret string, + entityID string, + processingChannelId string, +) *client { + var environment configuration.Environment + switch strings.ToLower(strings.TrimSpace(env)) { + case "sandbox": + environment = configuration.Sandbox() + default: + environment = configuration.Production() + } + + apiBase := environment.BaseUri() + apiAuthUrl := environment.AuthorizationUri() + + httpClient := &http.Client{ + Transport: &acceptHeaderTransport{ + base: metrics.NewTransport("checkout", metrics.TransportOpts{}), + }, + Timeout: 30 * time.Second, + } + + sdk, err := checkout.Builder(). + OAuth(). + WithClientCredentials(strings.TrimSpace(oauthClientID), strings.TrimSpace(oauthClientSecret)). + WithEnvironment(environment). + WithHttpClient(httpClient). + WithScopes(getOAuthScopes()). + Build() + if err != nil { + panic(err) + } + + return &client{ + sdk: sdk, + httpClient: httpClient, + apiBase: apiBase, + apiAuthUrl: apiAuthUrl, + oauthClientID: oauthClientID, + oauthClientSecret: oauthClientSecret, + entityID: entityID, + processingChannelId: processingChannelId, + } +} + +func getOAuthScopes() []string { + return []string{"accounts", "balances", "payments:search"} +} + +func (c *client) getAccessToken(ctx context.Context) (string, error) { + form := url.Values{} + form.Set("grant_type", "client_credentials") + form.Set("scope", strings.Join(getOAuthScopes(), " ")) + + req, err := http.NewRequestWithContext(ctx, http.MethodPost, strings.TrimSpace(c.apiAuthUrl), strings.NewReader(form.Encode())) + if err != nil { + return "", err + } + req.SetBasicAuth(strings.TrimSpace(c.oauthClientID), strings.TrimSpace(c.oauthClientSecret)) + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + + resp, err := c.httpClient.Do(req) + if err != nil { + return "", err + } + defer resp.Body.Close() + + if resp.StatusCode < 200 || resp.StatusCode > 299 { + var apiErr map[string]any + _ = json.NewDecoder(resp.Body).Decode(&apiErr) + return "", fmt.Errorf("oauth token %d: %v", resp.StatusCode, apiErr) + } + + var tok struct { + AccessToken string `json:"access_token"` + ExpiresIn int64 `json:"expires_in"` + TokenType string `json:"token_type"` + Scope string `json:"scope"` + } + if err := json.NewDecoder(resp.Body).Decode(&tok); err != nil { + return "", err + } + if tok.AccessToken == "" { + return "", fmt.Errorf("oauth token missing access_token") + } + return tok.AccessToken, nil +} diff --git a/internal/connectors/plugins/public/checkout/client/external_accounts.go b/internal/connectors/plugins/public/checkout/client/external_accounts.go new file mode 100644 index 00000000..5a1f9fbf --- /dev/null +++ b/internal/connectors/plugins/public/checkout/client/external_accounts.go @@ -0,0 +1,16 @@ +package client + +import ( + "context" + + "github.com/formancehq/payments/internal/connectors/metrics" +) + +type ExternalAccount struct {} + +func (c *client) GetExternalAccounts(ctx context.Context, page int, pageSize int) ([]*ExternalAccount, error) { + ctx = context.WithValue(ctx, metrics.MetricOperationContextKey, "list_external_accounts") + + // TODO: call PSP to fetch external accounts + return nil, nil +} diff --git a/internal/connectors/plugins/public/checkout/client/payouts.go b/internal/connectors/plugins/public/checkout/client/payouts.go new file mode 100644 index 00000000..34d0c827 --- /dev/null +++ b/internal/connectors/plugins/public/checkout/client/payouts.go @@ -0,0 +1,64 @@ +package client + +import ( + "context" + + "github.com/formancehq/payments/internal/connectors/metrics" + + "github.com/checkout/checkout-sdk-go/common" + "github.com/checkout/checkout-sdk-go/payments/nas" + "github.com/checkout/checkout-sdk-go/payments/nas/sources" +) + +type PayoutRequest struct { + SourceEntityID string + DestinationInstrumentID string + + Amount int64 + Currency string + BillingDescriptor string + Reference string + IdempotencyKey string +} + +type PayoutResponse struct { + ID string + Status string + Reference string +} + +func (c *client) InitiatePayout(ctx context.Context, pr *PayoutRequest) (*PayoutResponse, error) { + ctx = context.WithValue(ctx, metrics.MetricOperationContextKey, "initiate_payout") + + src := sources.NewRequestIdSource() + src.Id = pr.SourceEntityID + dest := nas.NewRequestIdDestination() + dest.Id = pr.DestinationInstrumentID + + var billing *nas.PayoutBillingDescriptor + if pr.BillingDescriptor != "" { + billing = &nas.PayoutBillingDescriptor{Reference: pr.BillingDescriptor} + } + + req := nas.PayoutRequest{ + Source: src, + Destination: dest, + Amount: pr.Amount, + Currency: common.Currency(pr.Currency), + Reference: pr.Reference, + ProcessingChannelId: c.processingChannelId, + BillingDescriptor: billing, + } + + res, err := c.sdk.Payments.RequestPayout(req, &pr.IdempotencyKey) + if err != nil { + return nil, err + } + + out := &PayoutResponse{ + ID: res.Id, + Status: string(res.Status), + Reference: res.Reference, + } + return out, nil +} diff --git a/internal/connectors/plugins/public/checkout/client/transactions.go b/internal/connectors/plugins/public/checkout/client/transactions.go new file mode 100644 index 00000000..d015c089 --- /dev/null +++ b/internal/connectors/plugins/public/checkout/client/transactions.go @@ -0,0 +1,130 @@ +package client + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "net/http" + "strings" + "time" + + "github.com/formancehq/payments/internal/connectors/metrics" +) + +type TransactionAction struct { + Type string `json:"type"` +} + +type Transaction struct { + ID string `json:"id"` + PaymentID string `json:"payment_id"` + Type string `json:"type"` + Status string `json:"status"` + Amount int64 `json:"amount"` + Currency string `json:"currency"` + Scheme string `json:"scheme"` + SourceAccountReference string `json:"sourceAccountReference"` + Actions []TransactionAction `json:"actions"` + CreatedAt time.Time `json:"created_at"` +} + +type searchSort struct { + Field string `json:"field"` + Order string `json:"order"` +} +type searchFilters struct { + EntityIDs []string `json:"entity_ids,omitempty"` +} +type searchPaymentsRequest struct { + Query string `json:"query,omitempty"` + From int `json:"from,omitempty"` + Limit int `json:"limit,omitempty"` + Sort []searchSort `json:"sort,omitempty"` + Filters *searchFilters `json:"filters,omitempty"` +} +type searchPaymentsResponse struct { + Data []struct { + ID string `json:"id"` + Amount int64 `json:"amount"` + Currency string `json:"currency"` + Status string `json:"status"` + Approved bool `json:"approved"` + Source struct { + Scheme string `json:"scheme"` + } `json:"source"` + RequestedOn time.Time `json:"requested_on"` + Actions []TransactionAction `json:"actions"` + } `json:"data"` +} + +func (c *client) GetTransactions(ctx context.Context, page, pageSize int) ([]*Transaction, error) { + ctx = context.WithValue(ctx, metrics.MetricOperationContextKey, "list_transactions") + + if page < 1 { + page = 1 + } + if pageSize <= 0 { + pageSize = 100 + } + + accessToken, err := c.getAccessToken(ctx) + if err != nil { + return nil, fmt.Errorf("oauth token: %w", err) + } + + reqBody := searchPaymentsRequest{ + Query: "", + Limit: pageSize, + } + body, _ := json.Marshal(reqBody) + + url := strings.TrimRight(c.apiBase, "/") + "/payments/search" + + fmt.Sprintf("PAYMENTS URL : %s", url) + + req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewReader(body)) + if err != nil { + return nil, fmt.Errorf("create search request: %w", err) + } + req.Header.Set("Authorization", "Bearer "+accessToken) + req.Header.Set("Content-Type", "application/json") + if req.Header.Get("Accept") == "" { + req.Header.Set("Accept", "application/json; schema_version=3.0") + } + + resp, err := c.httpClient.Do(req) + if err != nil { + return nil, fmt.Errorf("search payments http: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode < 200 || resp.StatusCode > 299 { + var apiErr map[string]any + _ = json.NewDecoder(resp.Body).Decode(&apiErr) + return nil, fmt.Errorf("search payments %d: %v", resp.StatusCode, apiErr) + } + + var sr searchPaymentsResponse + if err := json.NewDecoder(resp.Body).Decode(&sr); err != nil { + return nil, fmt.Errorf("decode search payments: %w", err) + } + + transactions := make([]*Transaction, 0, len(sr.Data)) + for _, it := range sr.Data { + transactions = append(transactions, &Transaction{ + ID: it.ID, + PaymentID: it.ID, + Type: "payment", + Scheme: it.Source.Scheme, + Status: it.Status, + Amount: it.Amount, + Currency: it.Currency, + SourceAccountReference: c.entityID, + Actions: it.Actions, + CreatedAt: it.RequestedOn, + }) + } + + return transactions, nil +} diff --git a/internal/connectors/plugins/public/checkout/client/transfers.go b/internal/connectors/plugins/public/checkout/client/transfers.go new file mode 100644 index 00000000..d32494b0 --- /dev/null +++ b/internal/connectors/plugins/public/checkout/client/transfers.go @@ -0,0 +1,73 @@ +package client + +import ( + "context" + "fmt" + "time" + + "github.com/checkout/checkout-sdk-go/common" + "github.com/checkout/checkout-sdk-go/transfers" + + "github.com/formancehq/payments/internal/connectors/metrics" +) + +type TransferRequest struct { + Reference string `json:"reference,omitempty"` + Reason string `json:"reason,omitempty"` + + Source struct { + EntityID string `json:"entity_id"` + Currency string `json:"currency"` + } `json:"source"` + + Destination struct { + EntityID string `json:"entity_id"` + Currency string `json:"currency"` + } `json:"destination"` + + Amount int64 `json:"amount"` + IdempotencyKey string `json:"-"` +} + +type TransferResponse struct { + ID string `json:"id"` + Status string `json:"status,omitempty"` + CreatedOn *time.Time `json:"created_on,omitempty"` + Raw any `json:"raw,omitempty"` +} + +type sdkTransferRequest = transfers.TransferRequest + +func (c *client) InitiateTransfer(ctx context.Context, tr *TransferRequest) (*TransferResponse, error) { + ctx = context.WithValue(ctx, metrics.MetricOperationContextKey, "initiate_transfer") + + req := transfers.TransferRequest{ + Reference: tr.Reference, + TransferType: "commission", + Source: &transfers.TransferSourceRequest{ + Id: tr.Source.EntityID, + Amount: tr.Amount, + Currency: common.Currency(tr.Source.Currency), + }, + Destination: &transfers.TransferDestinationRequest{ + Id: tr.Destination.EntityID, + }, + } + + var idem *string + if tr.IdempotencyKey != "" { + idem = &tr.IdempotencyKey + } + + resp, err := c.sdk.Transfers.InitiateTransferOfFounds(req, idem) + if err != nil { + return nil, fmt.Errorf("checkout.accounts.transfers: %w", err) + } + + out := &TransferResponse{ + ID: resp.Id, + Status: resp.Status, + Raw: resp, + } + return out, nil +} diff --git a/internal/connectors/plugins/public/checkout/config.go b/internal/connectors/plugins/public/checkout/config.go new file mode 100644 index 00000000..4f8b8c65 --- /dev/null +++ b/internal/connectors/plugins/public/checkout/config.go @@ -0,0 +1,29 @@ +package checkout + +import ( + "encoding/json" + "fmt" + + "github.com/go-playground/validator/v10" + "github.com/formancehq/payments/internal/models" +) + +type Config struct { + // This is the config a user will pass when installing this connector. + // Authentication criteria for connecting to your connector should be provided here. Example: + Environment string `json:"environment" validate:"required"` + OAuthClientID string `json:"oauthClientID" validate:"required"` + OAuthClientSecret string `json:"oauthClientSecret" validate:"required"` + EntityID string `json:"entityId" validate:"required"` + ProcessingChannelId string `json:"processingChannelId" validate:"required"` +} + +func unmarshalAndValidateConfig(payload json.RawMessage) (Config, error) { + var config Config + if err := json.Unmarshal(payload, &config); err != nil { + return Config{}, fmt.Errorf("%w: %w", err, models.ErrInvalidConfig) + } + + validate := validator.New(validator.WithRequiredStructEnabled()) + return config, validate.Struct(config) +} diff --git a/internal/connectors/plugins/public/checkout/currencies.go b/internal/connectors/plugins/public/checkout/currencies.go new file mode 100644 index 00000000..1aa10ef9 --- /dev/null +++ b/internal/connectors/plugins/public/checkout/currencies.go @@ -0,0 +1,15 @@ +package checkout + +import "github.com/formancehq/go-libs/v3/currency" + +var ( + // TODO: the next line tells that the connector is supporting all currencies. + // If you only want to support specific currencies, you will have to remove + // this line and set the map yourselves + // Example: + // supportedCurrenciesWithDecimal = map[string]int{ + // "EUR": currency.ISO4217Currencies["EUR"], // Euro + // "DKK": currency.ISO4217Currencies["DKK"], + // } + supportedCurrenciesWithDecimal = currency.ISO4217Currencies +) diff --git a/internal/connectors/plugins/public/checkout/external_accounts.go b/internal/connectors/plugins/public/checkout/external_accounts.go new file mode 100644 index 00000000..93982fe9 --- /dev/null +++ b/internal/connectors/plugins/public/checkout/external_accounts.go @@ -0,0 +1,76 @@ +package checkout + +import ( + "context" + "encoding/json" + + "github.com/formancehq/payments/internal/models" + "github.com/formancehq/payments/internal/utils/pagination" +) + +type externalAccountsState struct { + // TODO: externalAccountsState will be used to know at what point we're at + // when fetching the PSP external accounts. We highly recommend to use this + // state to not poll data already polled. + // This struct will be stored as a raw json, you're free to put whatever + // you want. + // Example: + // LastPage int `json:"lastPage"` + // LastIDCreated int64 `json:"lastIDCreated"` +} + +func (p *Plugin) fetchNextExternalAccounts(ctx context.Context, req models.FetchNextExternalAccountsRequest) (models.FetchNextExternalAccountsResponse, error) { + var oldState externalAccountsState + if req.State != nil { + if err := json.Unmarshal(req.State, &oldState); err != nil { + return models.FetchNextExternalAccountsResponse{}, err + } + } + + // TODO: if needed, uncomment the following lines to get the related account in request + // var from models.PSPAccount + // if req.FromPayload == nil { + // return models.FetchNextExternalAccountsResponse{}, models.ErrMissingFromPayloadInRequest + // } + // if err := json.Unmarshal(req.FromPayload, &from); err != nil { + // return models.FetchNextExternalAccountsResponse{}, err + // } + + newState := externalAccountsState{ + // TODO: fill new state with old state values + } + + needMore := false + hasMore := false + accounts := make([]models.PSPAccount, 0, req.PageSize) + for /* TODO: range over pages or others */ page := 0; ; page++ { + pagedRecipients, err := p.client.GetExternalAccounts(ctx, page, req.PageSize) + if err != nil { + return models.FetchNextExternalAccountsResponse{}, err + } + + // TODO: transfer PSP object into formance object + accounts = append(accounts, models.PSPAccount{}) + + needMore, hasMore = pagination.ShouldFetchMore(accounts, pagedRecipients, req.PageSize) + if !needMore || !hasMore { + break + } + } + + if !needMore { + accounts = accounts[:req.PageSize] + } + + // TODO: don't forget to update your state accordingly + payload, err := json.Marshal(newState) + if err != nil { + return models.FetchNextExternalAccountsResponse{}, err + } + + return models.FetchNextExternalAccountsResponse{ + ExternalAccounts: accounts, + NewState: payload, + HasMore: hasMore, + }, nil +} \ No newline at end of file diff --git a/internal/connectors/plugins/public/checkout/payments.go b/internal/connectors/plugins/public/checkout/payments.go new file mode 100644 index 00000000..b785477f --- /dev/null +++ b/internal/connectors/plugins/public/checkout/payments.go @@ -0,0 +1,176 @@ +package checkout + +import ( + "context" + "encoding/json" + "math/big" + "strings" + + "github.com/formancehq/payments/internal/models" + "github.com/formancehq/payments/internal/utils/pagination" + "github.com/formancehq/go-libs/v3/currency" +) + +type paymentsState struct { + LastPage int `json:"lastPage"` +} + +func mapCheckoutPaymentStatus(s string) models.PaymentStatus { + if s == "" { + return models.PAYMENT_STATUS_UNKNOWN + } + ls := strings.ToLower(strings.TrimSpace(s)) + + switch ls { + case "authorized", "authorised", "card verified", "approved": + return models.PAYMENT_STATUS_AUTHORISATION + case "captured", "capture", "partially captured": + return models.PAYMENT_STATUS_CAPTURE + case "refunded", "partially refunded": + return models.PAYMENT_STATUS_REFUNDED + case "pending", "capture pending", "refund pending": + return models.PAYMENT_STATUS_PENDING + case "declined", "failed", "failure": + return models.PAYMENT_STATUS_FAILED + case "expired": + return models.PAYMENT_STATUS_EXPIRED + case "canceled", "cancelled", "voided", "void": + return models.PAYMENT_STATUS_CANCELLED + case "refund declined", "refund_failed", "refund failed": + return models.PAYMENT_STATUS_REFUNDED_FAILURE + case "refund reversed", "reversed": + return models.PAYMENT_STATUS_REFUND_REVERSED + case "disputed", "chargeback": + return models.PAYMENT_STATUS_DISPUTE + case "chargeback won", "dispute won": + return models.PAYMENT_STATUS_DISPUTE_WON + case "chargeback lost", "dispute lost": + return models.PAYMENT_STATUS_DISPUTE_LOST + default: + return models.PAYMENT_STATUS_OTHER + } +} + +func mapCheckoutScheme(scheme string) models.PaymentScheme { + switch strings.ToUpper(scheme) { + case "VISA": + return models.PAYMENT_SCHEME_CARD_VISA + case "MASTERCARD": + return models.PAYMENT_SCHEME_CARD_MASTERCARD + case "AMEX", "AMERICAN_EXPRESS": + return models.PAYMENT_SCHEME_CARD_AMEX + case "DINERS": + return models.PAYMENT_SCHEME_CARD_DINERS + case "DISCOVER": + return models.PAYMENT_SCHEME_CARD_DISCOVER + case "JCB": + return models.PAYMENT_SCHEME_CARD_JCB + case "UNIONPAY", "UNION_PAY": + return models.PAYMENT_SCHEME_CARD_UNION_PAY + case "ALIPAY": + return models.PAYMENT_SCHEME_CARD_ALIPAY + case "CUP": + return models.PAYMENT_SCHEME_CARD_CUP + case "GOOGLEPAY", "GOOGLE_PAY": + return models.PAYMENT_SCHEME_GOOGLE_PAY + case "APPLEPAY", "APPLE_PAY": + return models.PAYMENT_SCHEME_APPLE_PAY + case "MAESTRO": + return models.PAYMENT_SCHEME_MAESTRO + case "ACH": + return models.PAYMENT_SCHEME_ACH + case "ACH_DEBIT": + return models.PAYMENT_SCHEME_ACH_DEBIT + case "RTP": + return models.PAYMENT_SCHEME_RTP + case "SEPA": + return models.PAYMENT_SCHEME_SEPA + case "SEPA_CREDIT": + return models.PAYMENT_SCHEME_SEPA_CREDIT + case "SEPA_DEBIT": + return models.PAYMENT_SCHEME_SEPA_DEBIT + default: + return models.PAYMENT_SCHEME_OTHER + } +} + +func (p *Plugin) fetchNextPayments(ctx context.Context, req models.FetchNextPaymentsRequest) (models.FetchNextPaymentsResponse, error) { + var oldState paymentsState + if req.State != nil { + if err := json.Unmarshal(req.State, &oldState); err != nil { + return models.FetchNextPaymentsResponse{}, err + } + } + + startPage := oldState.LastPage + 1 + newState := paymentsState{ + LastPage: oldState.LastPage, + } + + payments := make([]models.PSPPayment, 0, req.PageSize) + needMore := false + hasMore := false + + for page := startPage; ; page++ { + pagedTxs, err := p.client.GetTransactions(ctx, page, req.PageSize) + if err != nil { + return models.FetchNextPaymentsResponse{}, err + } + + for _, t := range pagedTxs { + raw, _ := json.Marshal(t) + + asset := currency.FormatAsset(supportedCurrenciesWithDecimal, t.Currency) + + md := map[string]string{ + "payment_id": t.PaymentID, + "type": t.Type, + "status": t.Status, + } + + paymentType := models.PAYMENT_TYPE_PAYIN + for _, act := range t.Actions { + if strings.EqualFold(act.Type, "Payout") { + paymentType = models.PAYMENT_TYPE_PAYOUT + break + } + } + + payments = append(payments, models.PSPPayment{ + ParentReference: "", + Reference: t.ID, + CreatedAt: t.CreatedAt, + Type: paymentType, + Amount: big.NewInt(t.Amount), + Asset: asset, + Scheme: mapCheckoutScheme(t.Scheme), + Status: mapCheckoutPaymentStatus(t.Status), + SourceAccountReference: &t.SourceAccountReference, + Metadata: md, + Raw: raw, + }) + } + + needMore, hasMore = pagination.ShouldFetchMore(payments, pagedTxs, req.PageSize) + newState.LastPage = page + + if !needMore || !hasMore { + break + } + } + + if !needMore && len(payments) > req.PageSize { + payments = payments[:req.PageSize] + } + + payload, err := json.Marshal(newState) + if err != nil { + return models.FetchNextPaymentsResponse{}, err + } + + return models.FetchNextPaymentsResponse{ + Payments: payments, + NewState: payload, + HasMore: hasMore, + }, nil +} diff --git a/internal/connectors/plugins/public/checkout/payouts.go b/internal/connectors/plugins/public/checkout/payouts.go new file mode 100644 index 00000000..7f0f6bb5 --- /dev/null +++ b/internal/connectors/plugins/public/checkout/payouts.go @@ -0,0 +1,58 @@ +package checkout + +import ( + "context" + "errors" + + "github.com/formancehq/payments/internal/connectors/plugins/public/checkout/client" + "github.com/formancehq/payments/internal/models" +) + +func (p *Plugin) createPayout(ctx context.Context, pi models.PSPPaymentInitiation) (*models.PSPPayment, error) { + if err := p.validateTransferPayoutRequests(pi); err != nil { + return nil, err + } + + var pr client.PayoutRequest + pr.Amount = pi.Amount.Int64() + pr.Currency = pi.Asset + pr.Reference = pi.Reference + pr.SourceEntityID = pi.SourceAccount.Reference + pr.DestinationInstrumentID = pi.DestinationAccount.Reference + pr.BillingDescriptor = pi.Description + pr.IdempotencyKey = p.generateIdempotencyKey(pi.Reference) + + resp, err := p.client.InitiatePayout(ctx, &pr) + if err != nil { + return nil, err + } + + return payoutToPayment(resp) +} + +func payoutToPayment(from *client.PayoutResponse) (*models.PSPPayment, error) { + if from == nil { + return nil, errors.New("nil payout response") + } + + p := &models.PSPPayment{ + Status: mapStatus(from), + Reference: from.Reference, + } + return p, nil +} + +func mapStatus(from *client.PayoutResponse) models.PaymentStatus { + switch from.Status { + case "Pending": + return models.PAYMENT_STATUS_PENDING + case "Captured", "Authorized", "Active": + return models.PAYMENT_STATUS_SUCCEEDED + case "Declined", "Failed", "Voided": + return models.PAYMENT_STATUS_FAILED + case "Canceled": + return models.PAYMENT_STATUS_CANCELLED + default: + return models.PAYMENT_STATUS_UNKNOWN + } +} \ No newline at end of file diff --git a/internal/connectors/plugins/public/checkout/plugin.go b/internal/connectors/plugins/public/checkout/plugin.go new file mode 100644 index 00000000..aea6ef40 --- /dev/null +++ b/internal/connectors/plugins/public/checkout/plugin.go @@ -0,0 +1,164 @@ +package checkout + +import ( + "context" + "encoding/json" + + "github.com/formancehq/go-libs/v3/logging" + "github.com/formancehq/payments/internal/connectors/plugins" + "github.com/formancehq/payments/internal/connectors/plugins/public/checkout/client" + "github.com/formancehq/payments/internal/connectors/plugins/registry" + "github.com/formancehq/payments/internal/models" +) + +const ProviderName = "checkout" + +func init() { + registry.RegisterPlugin(ProviderName, models.PluginTypePSP, func(_ models.ConnectorID, name string, logger logging.Logger, rm json.RawMessage) (models.Plugin, error) { + return New(name, logger, rm) + }, capabilities, Config{}) +} + +type Plugin struct { + models.Plugin + + name string + logger logging.Logger + + client client.Client +} + +func New(name string, logger logging.Logger, rawConfig json.RawMessage) (*Plugin, error) { + cfg, err := unmarshalAndValidateConfig(rawConfig) + if err != nil { + return nil, err + } + + cl := client.New( + cfg.Environment, + cfg.OAuthClientID, + cfg.OAuthClientSecret, + cfg.EntityID, + cfg.ProcessingChannelId, + ) + + return &Plugin{ + Plugin: plugins.NewBasePlugin(), + name: name, + logger: logger, + client: cl, + }, nil +} + + +func (p *Plugin) Name() string { + return p.name +} + +func (p *Plugin) Install(_ context.Context, req models.InstallRequest) (models.InstallResponse, error) { + return models.InstallResponse{ + Workflow: workflow(), + }, nil +} + +func (p *Plugin) Uninstall(ctx context.Context, req models.UninstallRequest) (models.UninstallResponse, error) { + return models.UninstallResponse{}, nil +} + +func (p *Plugin) FetchNextAccounts(ctx context.Context, req models.FetchNextAccountsRequest) (models.FetchNextAccountsResponse, error) { + if p.client == nil { + return models.FetchNextAccountsResponse{}, plugins.ErrNotYetInstalled + } + return p.fetchNextAccounts(ctx, req) +} + +func (p *Plugin) FetchNextBalances(ctx context.Context, req models.FetchNextBalancesRequest) (models.FetchNextBalancesResponse, error) { + if p.client == nil { + return models.FetchNextBalancesResponse{}, plugins.ErrNotYetInstalled + } + return p.fetchNextBalances(ctx, req) +} + +func (p *Plugin) FetchNextExternalAccounts(ctx context.Context, req models.FetchNextExternalAccountsRequest) (models.FetchNextExternalAccountsResponse, error) { + if p.client == nil { + return models.FetchNextExternalAccountsResponse{}, plugins.ErrNotYetInstalled + } + return p.fetchNextExternalAccounts(ctx, req) +} + +func (p *Plugin) FetchNextPayments(ctx context.Context, req models.FetchNextPaymentsRequest) (models.FetchNextPaymentsResponse, error) { + if p.client == nil { + return models.FetchNextPaymentsResponse{}, plugins.ErrNotYetInstalled + } + return p.fetchNextPayments(ctx, req) +} + +func (p *Plugin) FetchNextOthers(ctx context.Context, req models.FetchNextOthersRequest) (models.FetchNextOthersResponse, error) { + return models.FetchNextOthersResponse{}, plugins.ErrNotImplemented +} + +func (p *Plugin) CreateBankAccount(ctx context.Context, req models.CreateBankAccountRequest) (models.CreateBankAccountResponse, error) { + return models.CreateBankAccountResponse{}, plugins.ErrNotImplemented +} + +func (p *Plugin) CreateTransfer(ctx context.Context, req models.CreateTransferRequest) (models.CreateTransferResponse, error) { + if p.client == nil { + return models.CreateTransferResponse{}, plugins.ErrNotYetInstalled + } + + payment, err := p.createTransfer(ctx, req.PaymentInitiation) + if err != nil { + return models.CreateTransferResponse{}, err + } + + return models.CreateTransferResponse{ + Payment: payment, + }, nil +} + +func (p *Plugin) ReverseTransfer(ctx context.Context, req models.ReverseTransferRequest) (models.ReverseTransferResponse, error) { + return models.ReverseTransferResponse{}, plugins.ErrNotImplemented +} + +// Note: Fill only if we cannot have the related payment in the CreateTransfer method +func (p *Plugin) PollTransferStatus(ctx context.Context, req models.PollTransferStatusRequest) (models.PollTransferStatusResponse, error) { + return models.PollTransferStatusResponse{}, plugins.ErrNotImplemented +} + +func (p *Plugin) CreatePayout(ctx context.Context, req models.CreatePayoutRequest) (models.CreatePayoutResponse, error) { + if p.client == nil { + return models.CreatePayoutResponse{}, plugins.ErrNotYetInstalled + } + + payment, err := p.createPayout(ctx, req.PaymentInitiation) + if err != nil { + return models.CreatePayoutResponse{}, err + } + + return models.CreatePayoutResponse{ + Payment: payment, + }, nil +} + +func (p *Plugin) ReversePayout(ctx context.Context, req models.ReversePayoutRequest) (models.ReversePayoutResponse, error) { + return models.ReversePayoutResponse{}, plugins.ErrNotImplemented +} + +// Note: Fill only if we cannot have the related payment in the CreatePayout method +func (p *Plugin) PollPayoutStatus(ctx context.Context, req models.PollPayoutStatusRequest) (models.PollPayoutStatusResponse, error) { + return models.PollPayoutStatusResponse{}, plugins.ErrNotImplemented +} + +// Note: if the connector has webhooks, use this method to create the related +// webhooks on the PSP. +func (p *Plugin) CreateWebhooks(ctx context.Context, req models.CreateWebhooksRequest) (models.CreateWebhooksResponse, error) { + return models.CreateWebhooksResponse{}, plugins.ErrNotImplemented +} + +// Note: if the connector has webhooks, use this method to translate incoming +// webhooks to a formance object. +func (p *Plugin) TranslateWebhook(ctx context.Context, req models.TranslateWebhookRequest) (models.TranslateWebhookResponse, error) { + return models.TranslateWebhookResponse{}, plugins.ErrNotImplemented +} + +var _ models.Plugin = &Plugin{} diff --git a/internal/connectors/plugins/public/checkout/transfers.go b/internal/connectors/plugins/public/checkout/transfers.go new file mode 100644 index 00000000..14751fb4 --- /dev/null +++ b/internal/connectors/plugins/public/checkout/transfers.go @@ -0,0 +1,76 @@ +package checkout + +import ( + "context" + "encoding/json" + "strings" + "time" + "math/big" + + "github.com/formancehq/payments/internal/connectors/plugins/public/checkout/client" + "github.com/formancehq/payments/internal/models" + + "github.com/formancehq/go-libs/v3/currency" +) + +func (p *Plugin) createTransfer(ctx context.Context, pi models.PSPPaymentInitiation) (*models.PSPPayment, error) { + if err := p.validateTransferPayoutRequests(pi); err != nil { + return nil, err + } + + tr := &client.TransferRequest{ + Reference: pi.Reference, + Reason: "Formance transfer", + } + tr.Source.EntityID = pi.SourceAccount.Reference + tr.Source.Currency = pi.Asset + tr.Destination.EntityID = pi.DestinationAccount.Reference + tr.Destination.Currency = pi.Asset + tr.Amount = pi.Amount.Int64() + tr.IdempotencyKey = p.generateIdempotencyKey(pi.Reference) + + resp, err := p.client.InitiateTransfer(ctx, tr) + if err != nil { + return nil, err + } + + raw, _ := json.Marshal(resp) + + createdAt := time.Now().UTC() + if resp.CreatedOn != nil { + createdAt = *resp.CreatedOn + } + + asset := currency.FormatAsset(supportedCurrenciesWithDecimal, tr.Source.Currency) + + return &models.PSPPayment{ + ParentReference: "", + Reference: resp.ID, + CreatedAt: createdAt, + Type: models.PAYMENT_TYPE_TRANSFER, + Status: mapTransferStatusToPaymentStatus(resp.Status), + Amount: big.NewInt(tr.Amount), + Asset: asset, + Raw: raw, + }, nil +} + +func mapTransferStatusToPaymentStatus(s string) models.PaymentStatus { + ls := strings.ToLower(strings.TrimSpace(s)) + switch ls { + case "pending", "requested", "processing": + return models.PAYMENT_STATUS_PENDING + case "approved", "succeeded", "completed", "successful": + return models.PAYMENT_STATUS_SUCCEEDED + case "failed", "declined", "rejected", "error": + return models.PAYMENT_STATUS_FAILED + case "canceled", "cancelled", "voided": + return models.PAYMENT_STATUS_CANCELLED + case "reversed": + return models.PAYMENT_STATUS_REFUND_REVERSED + default: + return models.PAYMENT_STATUS_OTHER + } +} + +func strPtr(s string) *string { return &s } diff --git a/internal/connectors/plugins/public/checkout/utils.go b/internal/connectors/plugins/public/checkout/utils.go new file mode 100644 index 00000000..6705cb36 --- /dev/null +++ b/internal/connectors/plugins/public/checkout/utils.go @@ -0,0 +1,28 @@ +package checkout + +import ( + "crypto/sha256" + "encoding/hex" + "fmt" + "strings" + + "github.com/formancehq/payments/internal/models" +) + +func (p *Plugin) validateTransferPayoutRequests(pi models.PSPPaymentInitiation) error { + if pi.SourceAccount == nil { + return fmt.Errorf("source account is required: %w", models.ErrInvalidRequest) + } + + if pi.DestinationAccount == nil { + return fmt.Errorf("destination account is required: %w", models.ErrInvalidRequest) + } + + return nil +} + +func (p *Plugin) generateIdempotencyKey(values ...string) string { + joined := strings.Join(values, "-") + hash := sha256.Sum256([]byte(joined)) + return hex.EncodeToString(hash[:]) +} \ No newline at end of file diff --git a/internal/connectors/plugins/public/checkout/workflow.go b/internal/connectors/plugins/public/checkout/workflow.go new file mode 100644 index 00000000..5ad3f711 --- /dev/null +++ b/internal/connectors/plugins/public/checkout/workflow.go @@ -0,0 +1,27 @@ +package checkout + +import "github.com/formancehq/payments/internal/models" + +func workflow() models.ConnectorTasksTree { + return []models.ConnectorTaskTree{ + { + + TaskType: models.TASK_FETCH_ACCOUNTS, + Name: "fetch_accounts", + Periodically: true, + NextTasks: []models.ConnectorTaskTree{}, + }, + { + TaskType: models.TASK_FETCH_BALANCES, + Name: "fetch_balances", + Periodically: true, + NextTasks: []models.ConnectorTaskTree{}, + }, + { + TaskType: models.TASK_FETCH_PAYMENTS, + Name: "fetch_payments", + Periodically: true, + NextTasks: []models.ConnectorTaskTree{}, + }, + } +} \ No newline at end of file diff --git a/internal/connectors/plugins/public/list.go b/internal/connectors/plugins/public/list.go index 7f919fb5..ab045929 100644 --- a/internal/connectors/plugins/public/list.go +++ b/internal/connectors/plugins/public/list.go @@ -4,6 +4,7 @@ import ( _ "github.com/formancehq/payments/internal/connectors/plugins/public/adyen" _ "github.com/formancehq/payments/internal/connectors/plugins/public/atlar" _ "github.com/formancehq/payments/internal/connectors/plugins/public/bankingcircle" + _ "github.com/formancehq/payments/internal/connectors/plugins/public/checkout" _ "github.com/formancehq/payments/internal/connectors/plugins/public/column" _ "github.com/formancehq/payments/internal/connectors/plugins/public/currencycloud" _ "github.com/formancehq/payments/internal/connectors/plugins/public/dummypay" diff --git a/openapi.yaml b/openapi.yaml index d4ac7ae8..306af810 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -5125,6 +5125,7 @@ components: Adyen: '#/components/schemas/V3AdyenConfig' Atlar: '#/components/schemas/V3AtlarConfig' Bankingcircle: '#/components/schemas/V3BankingcircleConfig' + Checkout: '#/components/schemas/V3CheckoutConfig' Column: '#/components/schemas/V3ColumnConfig' Currencycloud: '#/components/schemas/V3CurrencycloudConfig' Dummypay: '#/components/schemas/V3DummypayConfig' @@ -5142,6 +5143,7 @@ components: - $ref: '#/components/schemas/V3AdyenConfig' - $ref: '#/components/schemas/V3AtlarConfig' - $ref: '#/components/schemas/V3BankingcircleConfig' + - $ref: '#/components/schemas/V3CheckoutConfig' - $ref: '#/components/schemas/V3ColumnConfig' - $ref: '#/components/schemas/V3CurrencycloudConfig' - $ref: '#/components/schemas/V3DummypayConfig' @@ -5242,6 +5244,37 @@ components: type: string username: type: string + V3CheckoutConfig: + type: object + required: + - name + - environment + - oauthClientID + - oauthClientSecret + - entityId + - processingChannelId + properties: + entityId: + type: string + environment: + type: string + name: + type: string + oauthClientID: + type: string + oauthClientSecret: + type: string + pageSize: + type: integer + default: "25" + pollingPeriod: + type: string + default: 2m + processingChannelId: + type: string + provider: + type: string + default: Checkout V3ColumnConfig: type: object required: diff --git a/openapi/v3/v3-connectors-config.yaml b/openapi/v3/v3-connectors-config.yaml index 70e3519f..78775d5f 100644 --- a/openapi/v3/v3-connectors-config.yaml +++ b/openapi/v3/v3-connectors-config.yaml @@ -7,6 +7,7 @@ components: Adyen: '#/components/schemas/V3AdyenConfig' Atlar: '#/components/schemas/V3AtlarConfig' Bankingcircle: '#/components/schemas/V3BankingcircleConfig' + Checkout: '#/components/schemas/V3CheckoutConfig' Column: '#/components/schemas/V3ColumnConfig' Currencycloud: '#/components/schemas/V3CurrencycloudConfig' Dummypay: '#/components/schemas/V3DummypayConfig' @@ -24,6 +25,7 @@ components: - $ref: '#/components/schemas/V3AdyenConfig' - $ref: '#/components/schemas/V3AtlarConfig' - $ref: '#/components/schemas/V3BankingcircleConfig' + - $ref: '#/components/schemas/V3CheckoutConfig' - $ref: '#/components/schemas/V3ColumnConfig' - $ref: '#/components/schemas/V3CurrencycloudConfig' - $ref: '#/components/schemas/V3DummypayConfig' @@ -124,6 +126,37 @@ components: type: string username: type: string + V3CheckoutConfig: + type: object + required: + - name + - environment + - oauthClientID + - oauthClientSecret + - entityId + - processingChannelId + properties: + entityId: + type: string + environment: + type: string + name: + type: string + oauthClientID: + type: string + oauthClientSecret: + type: string + pageSize: + type: integer + default: "25" + pollingPeriod: + type: string + default: 2m + processingChannelId: + type: string + provider: + type: string + default: Checkout V3ColumnConfig: type: object required: diff --git a/pkg/client/.speakeasy/gen.lock b/pkg/client/.speakeasy/gen.lock index c586d001..73480268 100644 --- a/pkg/client/.speakeasy/gen.lock +++ b/pkg/client/.speakeasy/gen.lock @@ -1,7 +1,7 @@ lockVersion: 2.0.0 id: 1fa8a26f-45d9-44b7-8b97-fbeebcdcd8b1 management: - docChecksum: 6f38fd737465b7a4c97ae7e0794b7b8c + docChecksum: dd8764ef73990fc8d6eaa115b4ea8507 docVersion: v1 speakeasyVersion: 1.525.0 generationVersion: 2.562.2 @@ -119,6 +119,7 @@ generatedFiles: - /models/components/v3bankaccountrelatedaccount.go - /models/components/v3bankaccountscursorresponse.go - /models/components/v3bankingcircleconfig.go + - /models/components/v3checkoutconfig.go - /models/components/v3columnconfig.go - /models/components/v3connector.go - /models/components/v3connectorconfig.go @@ -421,6 +422,7 @@ generatedFiles: - docs/models/components/v3bankaccountscursorresponse.md - docs/models/components/v3bankaccountscursorresponsecursor.md - docs/models/components/v3bankingcircleconfig.md + - docs/models/components/v3checkoutconfig.md - docs/models/components/v3columnconfig.md - docs/models/components/v3connector.md - docs/models/components/v3connectorconfig.md diff --git a/pkg/client/docs/models/components/v3checkoutconfig.md b/pkg/client/docs/models/components/v3checkoutconfig.md new file mode 100644 index 00000000..5280302b --- /dev/null +++ b/pkg/client/docs/models/components/v3checkoutconfig.md @@ -0,0 +1,16 @@ +# V3CheckoutConfig + + +## Fields + +| Field | Type | Required | Description | +| --------------------- | --------------------- | --------------------- | --------------------- | +| `EntityID` | *string* | :heavy_check_mark: | N/A | +| `Environment` | *string* | :heavy_check_mark: | N/A | +| `Name` | *string* | :heavy_check_mark: | N/A | +| `OauthClientID` | *string* | :heavy_check_mark: | N/A | +| `OauthClientSecret` | *string* | :heavy_check_mark: | N/A | +| `PageSize` | **int64* | :heavy_minus_sign: | N/A | +| `PollingPeriod` | **string* | :heavy_minus_sign: | N/A | +| `ProcessingChannelID` | *string* | :heavy_check_mark: | N/A | +| `Provider` | **string* | :heavy_minus_sign: | N/A | \ No newline at end of file diff --git a/pkg/client/docs/models/components/v3connectorconfig.md b/pkg/client/docs/models/components/v3connectorconfig.md index b41f700d..13725724 100644 --- a/pkg/client/docs/models/components/v3connectorconfig.md +++ b/pkg/client/docs/models/components/v3connectorconfig.md @@ -21,6 +21,12 @@ v3ConnectorConfig := components.CreateV3ConnectorConfigAtlar(components.V3AtlarC v3ConnectorConfig := components.CreateV3ConnectorConfigBankingcircle(components.V3BankingcircleConfig{/* values here */}) ``` +### V3CheckoutConfig + +```go +v3ConnectorConfig := components.CreateV3ConnectorConfigCheckout(components.V3CheckoutConfig{/* values here */}) +``` + ### V3ColumnConfig ```go diff --git a/pkg/client/docs/models/components/v3installconnectorrequest.md b/pkg/client/docs/models/components/v3installconnectorrequest.md index 19281bc7..296d889c 100644 --- a/pkg/client/docs/models/components/v3installconnectorrequest.md +++ b/pkg/client/docs/models/components/v3installconnectorrequest.md @@ -21,6 +21,12 @@ v3InstallConnectorRequest := components.CreateV3InstallConnectorRequestAtlar(com v3InstallConnectorRequest := components.CreateV3InstallConnectorRequestBankingcircle(components.V3BankingcircleConfig{/* values here */}) ``` +### V3CheckoutConfig + +```go +v3InstallConnectorRequest := components.CreateV3InstallConnectorRequestCheckout(components.V3CheckoutConfig{/* values here */}) +``` + ### V3ColumnConfig ```go diff --git a/pkg/client/docs/models/components/v3updateconnectorrequest.md b/pkg/client/docs/models/components/v3updateconnectorrequest.md index dc284bf9..e3117990 100644 --- a/pkg/client/docs/models/components/v3updateconnectorrequest.md +++ b/pkg/client/docs/models/components/v3updateconnectorrequest.md @@ -21,6 +21,12 @@ v3UpdateConnectorRequest := components.CreateV3UpdateConnectorRequestAtlar(compo v3UpdateConnectorRequest := components.CreateV3UpdateConnectorRequestBankingcircle(components.V3BankingcircleConfig{/* values here */}) ``` +### V3CheckoutConfig + +```go +v3UpdateConnectorRequest := components.CreateV3UpdateConnectorRequestCheckout(components.V3CheckoutConfig{/* values here */}) +``` + ### V3ColumnConfig ```go diff --git a/pkg/client/models/components/v3checkoutconfig.go b/pkg/client/models/components/v3checkoutconfig.go new file mode 100644 index 00000000..d63add3f --- /dev/null +++ b/pkg/client/models/components/v3checkoutconfig.go @@ -0,0 +1,93 @@ +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package components + +import ( + "github.com/formancehq/payments/pkg/client/internal/utils" +) + +type V3CheckoutConfig struct { + EntityID string `json:"entityId"` + Environment string `json:"environment"` + Name string `json:"name"` + OauthClientID string `json:"oauthClientID"` + OauthClientSecret string `json:"oauthClientSecret"` + PageSize *int64 `default:"25" json:"pageSize"` + PollingPeriod *string `default:"2m" json:"pollingPeriod"` + ProcessingChannelID string `json:"processingChannelId"` + Provider *string `default:"Checkout" json:"provider"` +} + +func (v V3CheckoutConfig) MarshalJSON() ([]byte, error) { + return utils.MarshalJSON(v, "", false) +} + +func (v *V3CheckoutConfig) UnmarshalJSON(data []byte) error { + if err := utils.UnmarshalJSON(data, &v, "", false, false); err != nil { + return err + } + return nil +} + +func (o *V3CheckoutConfig) GetEntityID() string { + if o == nil { + return "" + } + return o.EntityID +} + +func (o *V3CheckoutConfig) GetEnvironment() string { + if o == nil { + return "" + } + return o.Environment +} + +func (o *V3CheckoutConfig) GetName() string { + if o == nil { + return "" + } + return o.Name +} + +func (o *V3CheckoutConfig) GetOauthClientID() string { + if o == nil { + return "" + } + return o.OauthClientID +} + +func (o *V3CheckoutConfig) GetOauthClientSecret() string { + if o == nil { + return "" + } + return o.OauthClientSecret +} + +func (o *V3CheckoutConfig) GetPageSize() *int64 { + if o == nil { + return nil + } + return o.PageSize +} + +func (o *V3CheckoutConfig) GetPollingPeriod() *string { + if o == nil { + return nil + } + return o.PollingPeriod +} + +func (o *V3CheckoutConfig) GetProcessingChannelID() string { + if o == nil { + return "" + } + return o.ProcessingChannelID +} + +func (o *V3CheckoutConfig) GetProvider() *string { + if o == nil { + return nil + } + return o.Provider +} diff --git a/pkg/client/models/components/v3connectorconfig.go b/pkg/client/models/components/v3connectorconfig.go index bb03414c..b34f287a 100644 --- a/pkg/client/models/components/v3connectorconfig.go +++ b/pkg/client/models/components/v3connectorconfig.go @@ -15,6 +15,7 @@ const ( V3ConnectorConfigTypeAdyen V3ConnectorConfigType = "Adyen" V3ConnectorConfigTypeAtlar V3ConnectorConfigType = "Atlar" V3ConnectorConfigTypeBankingcircle V3ConnectorConfigType = "Bankingcircle" + V3ConnectorConfigTypeCheckout V3ConnectorConfigType = "Checkout" V3ConnectorConfigTypeColumn V3ConnectorConfigType = "Column" V3ConnectorConfigTypeCurrencycloud V3ConnectorConfigType = "Currencycloud" V3ConnectorConfigTypeDummypay V3ConnectorConfigType = "Dummypay" @@ -34,6 +35,7 @@ type V3ConnectorConfig struct { V3AdyenConfig *V3AdyenConfig `queryParam:"inline"` V3AtlarConfig *V3AtlarConfig `queryParam:"inline"` V3BankingcircleConfig *V3BankingcircleConfig `queryParam:"inline"` + V3CheckoutConfig *V3CheckoutConfig `queryParam:"inline"` V3ColumnConfig *V3ColumnConfig `queryParam:"inline"` V3CurrencycloudConfig *V3CurrencycloudConfig `queryParam:"inline"` V3DummypayConfig *V3DummypayConfig `queryParam:"inline"` @@ -87,6 +89,18 @@ func CreateV3ConnectorConfigBankingcircle(bankingcircle V3BankingcircleConfig) V } } +func CreateV3ConnectorConfigCheckout(checkout V3CheckoutConfig) V3ConnectorConfig { + typ := V3ConnectorConfigTypeCheckout + + typStr := string(typ) + checkout.Provider = &typStr + + return V3ConnectorConfig{ + V3CheckoutConfig: &checkout, + Type: typ, + } +} + func CreateV3ConnectorConfigColumn(column V3ColumnConfig) V3ConnectorConfig { typ := V3ConnectorConfigTypeColumn @@ -282,6 +296,15 @@ func (u *V3ConnectorConfig) UnmarshalJSON(data []byte) error { u.V3BankingcircleConfig = v3BankingcircleConfig u.Type = V3ConnectorConfigTypeBankingcircle return nil + case "Checkout": + v3CheckoutConfig := new(V3CheckoutConfig) + if err := utils.UnmarshalJSON(data, &v3CheckoutConfig, "", true, false); err != nil { + return fmt.Errorf("could not unmarshal `%s` into expected (Provider == Checkout) type V3CheckoutConfig within V3ConnectorConfig: %w", string(data), err) + } + + u.V3CheckoutConfig = v3CheckoutConfig + u.Type = V3ConnectorConfigTypeCheckout + return nil case "Column": v3ColumnConfig := new(V3ColumnConfig) if err := utils.UnmarshalJSON(data, &v3ColumnConfig, "", true, false); err != nil { @@ -417,6 +440,10 @@ func (u V3ConnectorConfig) MarshalJSON() ([]byte, error) { return utils.MarshalJSON(u.V3BankingcircleConfig, "", true) } + if u.V3CheckoutConfig != nil { + return utils.MarshalJSON(u.V3CheckoutConfig, "", true) + } + if u.V3ColumnConfig != nil { return utils.MarshalJSON(u.V3ColumnConfig, "", true) } diff --git a/pkg/client/models/components/v3getconnectorconfigresponse.go b/pkg/client/models/components/v3getconnectorconfigresponse.go index a7b67f5a..3fab6a89 100644 --- a/pkg/client/models/components/v3getconnectorconfigresponse.go +++ b/pkg/client/models/components/v3getconnectorconfigresponse.go @@ -25,6 +25,10 @@ func (o *V3GetConnectorConfigResponse) GetDataBankingcircle() *V3BankingcircleCo return o.GetData().V3BankingcircleConfig } +func (o *V3GetConnectorConfigResponse) GetDataCheckout() *V3CheckoutConfig { + return o.GetData().V3CheckoutConfig +} + func (o *V3GetConnectorConfigResponse) GetDataColumn() *V3ColumnConfig { return o.GetData().V3ColumnConfig } diff --git a/pkg/client/models/components/v3installconnectorrequest.go b/pkg/client/models/components/v3installconnectorrequest.go index 0ca8261d..38195516 100644 --- a/pkg/client/models/components/v3installconnectorrequest.go +++ b/pkg/client/models/components/v3installconnectorrequest.go @@ -15,6 +15,7 @@ const ( V3InstallConnectorRequestTypeAdyen V3InstallConnectorRequestType = "Adyen" V3InstallConnectorRequestTypeAtlar V3InstallConnectorRequestType = "Atlar" V3InstallConnectorRequestTypeBankingcircle V3InstallConnectorRequestType = "Bankingcircle" + V3InstallConnectorRequestTypeCheckout V3InstallConnectorRequestType = "Checkout" V3InstallConnectorRequestTypeColumn V3InstallConnectorRequestType = "Column" V3InstallConnectorRequestTypeCurrencycloud V3InstallConnectorRequestType = "Currencycloud" V3InstallConnectorRequestTypeDummypay V3InstallConnectorRequestType = "Dummypay" @@ -34,6 +35,7 @@ type V3InstallConnectorRequest struct { V3AdyenConfig *V3AdyenConfig `queryParam:"inline"` V3AtlarConfig *V3AtlarConfig `queryParam:"inline"` V3BankingcircleConfig *V3BankingcircleConfig `queryParam:"inline"` + V3CheckoutConfig *V3CheckoutConfig `queryParam:"inline"` V3ColumnConfig *V3ColumnConfig `queryParam:"inline"` V3CurrencycloudConfig *V3CurrencycloudConfig `queryParam:"inline"` V3DummypayConfig *V3DummypayConfig `queryParam:"inline"` @@ -87,6 +89,18 @@ func CreateV3InstallConnectorRequestBankingcircle(bankingcircle V3BankingcircleC } } +func CreateV3InstallConnectorRequestCheckout(checkout V3CheckoutConfig) V3InstallConnectorRequest { + typ := V3InstallConnectorRequestTypeCheckout + + typStr := string(typ) + checkout.Provider = &typStr + + return V3InstallConnectorRequest{ + V3CheckoutConfig: &checkout, + Type: typ, + } +} + func CreateV3InstallConnectorRequestColumn(column V3ColumnConfig) V3InstallConnectorRequest { typ := V3InstallConnectorRequestTypeColumn @@ -282,6 +296,15 @@ func (u *V3InstallConnectorRequest) UnmarshalJSON(data []byte) error { u.V3BankingcircleConfig = v3BankingcircleConfig u.Type = V3InstallConnectorRequestTypeBankingcircle return nil + case "Checkout": + v3CheckoutConfig := new(V3CheckoutConfig) + if err := utils.UnmarshalJSON(data, &v3CheckoutConfig, "", true, false); err != nil { + return fmt.Errorf("could not unmarshal `%s` into expected (Provider == Checkout) type V3CheckoutConfig within V3InstallConnectorRequest: %w", string(data), err) + } + + u.V3CheckoutConfig = v3CheckoutConfig + u.Type = V3InstallConnectorRequestTypeCheckout + return nil case "Column": v3ColumnConfig := new(V3ColumnConfig) if err := utils.UnmarshalJSON(data, &v3ColumnConfig, "", true, false); err != nil { @@ -417,6 +440,10 @@ func (u V3InstallConnectorRequest) MarshalJSON() ([]byte, error) { return utils.MarshalJSON(u.V3BankingcircleConfig, "", true) } + if u.V3CheckoutConfig != nil { + return utils.MarshalJSON(u.V3CheckoutConfig, "", true) + } + if u.V3ColumnConfig != nil { return utils.MarshalJSON(u.V3ColumnConfig, "", true) } diff --git a/pkg/client/models/components/v3updateconnectorrequest.go b/pkg/client/models/components/v3updateconnectorrequest.go index 0f5c22e8..b5e12f57 100644 --- a/pkg/client/models/components/v3updateconnectorrequest.go +++ b/pkg/client/models/components/v3updateconnectorrequest.go @@ -15,6 +15,7 @@ const ( V3UpdateConnectorRequestTypeAdyen V3UpdateConnectorRequestType = "Adyen" V3UpdateConnectorRequestTypeAtlar V3UpdateConnectorRequestType = "Atlar" V3UpdateConnectorRequestTypeBankingcircle V3UpdateConnectorRequestType = "Bankingcircle" + V3UpdateConnectorRequestTypeCheckout V3UpdateConnectorRequestType = "Checkout" V3UpdateConnectorRequestTypeColumn V3UpdateConnectorRequestType = "Column" V3UpdateConnectorRequestTypeCurrencycloud V3UpdateConnectorRequestType = "Currencycloud" V3UpdateConnectorRequestTypeDummypay V3UpdateConnectorRequestType = "Dummypay" @@ -34,6 +35,7 @@ type V3UpdateConnectorRequest struct { V3AdyenConfig *V3AdyenConfig `queryParam:"inline"` V3AtlarConfig *V3AtlarConfig `queryParam:"inline"` V3BankingcircleConfig *V3BankingcircleConfig `queryParam:"inline"` + V3CheckoutConfig *V3CheckoutConfig `queryParam:"inline"` V3ColumnConfig *V3ColumnConfig `queryParam:"inline"` V3CurrencycloudConfig *V3CurrencycloudConfig `queryParam:"inline"` V3DummypayConfig *V3DummypayConfig `queryParam:"inline"` @@ -87,6 +89,18 @@ func CreateV3UpdateConnectorRequestBankingcircle(bankingcircle V3BankingcircleCo } } +func CreateV3UpdateConnectorRequestCheckout(checkout V3CheckoutConfig) V3UpdateConnectorRequest { + typ := V3UpdateConnectorRequestTypeCheckout + + typStr := string(typ) + checkout.Provider = &typStr + + return V3UpdateConnectorRequest{ + V3CheckoutConfig: &checkout, + Type: typ, + } +} + func CreateV3UpdateConnectorRequestColumn(column V3ColumnConfig) V3UpdateConnectorRequest { typ := V3UpdateConnectorRequestTypeColumn @@ -282,6 +296,15 @@ func (u *V3UpdateConnectorRequest) UnmarshalJSON(data []byte) error { u.V3BankingcircleConfig = v3BankingcircleConfig u.Type = V3UpdateConnectorRequestTypeBankingcircle return nil + case "Checkout": + v3CheckoutConfig := new(V3CheckoutConfig) + if err := utils.UnmarshalJSON(data, &v3CheckoutConfig, "", true, false); err != nil { + return fmt.Errorf("could not unmarshal `%s` into expected (Provider == Checkout) type V3CheckoutConfig within V3UpdateConnectorRequest: %w", string(data), err) + } + + u.V3CheckoutConfig = v3CheckoutConfig + u.Type = V3UpdateConnectorRequestTypeCheckout + return nil case "Column": v3ColumnConfig := new(V3ColumnConfig) if err := utils.UnmarshalJSON(data, &v3ColumnConfig, "", true, false); err != nil { @@ -417,6 +440,10 @@ func (u V3UpdateConnectorRequest) MarshalJSON() ([]byte, error) { return utils.MarshalJSON(u.V3BankingcircleConfig, "", true) } + if u.V3CheckoutConfig != nil { + return utils.MarshalJSON(u.V3CheckoutConfig, "", true) + } + if u.V3ColumnConfig != nil { return utils.MarshalJSON(u.V3ColumnConfig, "", true) } diff --git a/pkg/client/models/operations/v3installconnector.go b/pkg/client/models/operations/v3installconnector.go index 2fa914a6..485e1a29 100644 --- a/pkg/client/models/operations/v3installconnector.go +++ b/pkg/client/models/operations/v3installconnector.go @@ -47,6 +47,13 @@ func (o *V3InstallConnectorRequest) GetV3InstallConnectorRequestBankingcircle() return nil } +func (o *V3InstallConnectorRequest) GetV3InstallConnectorRequestCheckout() *components.V3CheckoutConfig { + if v := o.GetV3InstallConnectorRequest(); v != nil { + return v.V3CheckoutConfig + } + return nil +} + func (o *V3InstallConnectorRequest) GetV3InstallConnectorRequestColumn() *components.V3ColumnConfig { if v := o.GetV3InstallConnectorRequest(); v != nil { return v.V3ColumnConfig diff --git a/pkg/client/models/operations/v3updateconnectorconfig.go b/pkg/client/models/operations/v3updateconnectorconfig.go index 5dd0cd7d..8a87a0f7 100644 --- a/pkg/client/models/operations/v3updateconnectorconfig.go +++ b/pkg/client/models/operations/v3updateconnectorconfig.go @@ -47,6 +47,13 @@ func (o *V3UpdateConnectorConfigRequest) GetV3UpdateConnectorRequestBankingcircl return nil } +func (o *V3UpdateConnectorConfigRequest) GetV3UpdateConnectorRequestCheckout() *components.V3CheckoutConfig { + if v := o.GetV3UpdateConnectorRequest(); v != nil { + return v.V3CheckoutConfig + } + return nil +} + func (o *V3UpdateConnectorConfigRequest) GetV3UpdateConnectorRequestColumn() *components.V3ColumnConfig { if v := o.GetV3UpdateConnectorRequest(); v != nil { return v.V3ColumnConfig