From 318856887d93eb3e9bd3de2d0c82ef63f3d4795b Mon Sep 17 00:00:00 2001 From: Paul Nicolas Date: Wed, 23 Apr 2025 13:46:50 +0200 Subject: [PATCH 01/13] chore(storage): remove errors.Wrap deprecated --- internal/storage/connector_tasks_tree.go | 6 +++--- internal/storage/connectors.go | 10 +++++----- internal/storage/payment_initiation_reversals.go | 4 ++-- internal/storage/payment_initiations.go | 6 +++--- internal/storage/payments.go | 7 +++---- internal/storage/schedules.go | 3 +-- internal/storage/workflow_instances.go | 5 ++--- 7 files changed, 19 insertions(+), 22 deletions(-) diff --git a/internal/storage/connector_tasks_tree.go b/internal/storage/connector_tasks_tree.go index a9a2ddc3d..194d3319f 100644 --- a/internal/storage/connector_tasks_tree.go +++ b/internal/storage/connector_tasks_tree.go @@ -3,9 +3,9 @@ package storage import ( "context" "encoding/json" + "fmt" "github.com/formancehq/payments/internal/models" - "github.com/pkg/errors" "github.com/uptrace/bun" ) @@ -20,7 +20,7 @@ type connectorTasksTree struct { func (s *store) ConnectorTasksTreeUpsert(ctx context.Context, connectorID models.ConnectorID, ts models.ConnectorTasksTree) error { payload, err := json.Marshal(&ts) if err != nil { - return errors.Wrap(err, "failed to marshal tasks") + return fmt.Errorf("failed to marshal tasks: %w", err) } tasks := connectorTasksTree{ @@ -49,7 +49,7 @@ func (s *store) ConnectorTasksTreeGet(ctx context.Context, connectorID models.Co var tasks models.ConnectorTasksTree if err := json.Unmarshal(ts.TasksTree, &tasks); err != nil { - return nil, errors.Wrap(err, "failed to unmarshal tasks") + return nil, fmt.Errorf("failed to unmarshal tasks: %w", err) } return &tasks, nil diff --git a/internal/storage/connectors.go b/internal/storage/connectors.go index 7382765ff..15c92b726 100644 --- a/internal/storage/connectors.go +++ b/internal/storage/connectors.go @@ -51,7 +51,7 @@ type connector struct { func (s *store) ListenConnectorsChanges(ctx context.Context, handlers HandlerConnectorsChanges) error { conn, err := s.db.Conn(ctx) if err != nil { - return errors.Wrap(err, "cannot get connection") + return fmt.Errorf("cannot get connection: %w", err) } s.rwMutex.Lock() @@ -87,7 +87,7 @@ func (s *store) ListenConnectorsChanges(ctx context.Context, handlers HandlerCon }() return nil }); err != nil { - return errors.Wrap(err, "cannot get driver connection") + return fmt.Errorf("cannot get driver connection: %w", err) } return nil } @@ -95,7 +95,7 @@ func (s *store) ListenConnectorsChanges(ctx context.Context, handlers HandlerCon func (s *store) ConnectorsInstall(ctx context.Context, c models.Connector) error { tx, err := s.db.BeginTx(ctx, &sql.TxOptions{}) if err != nil { - return errors.Wrap(err, "cannot begin transaction") + return fmt.Errorf("cannot begin transaction: %w", err) } defer func() { rollbackOnTxError(ctx, &tx, err) @@ -133,7 +133,7 @@ func (s *store) ConnectorsInstall(ctx context.Context, c models.Connector) error func (s *store) ConnectorsConfigUpdate(ctx context.Context, c models.Connector) error { tx, err := s.db.BeginTx(ctx, &sql.TxOptions{}) if err != nil { - return errors.Wrap(err, "cannot begin transaction") + return fmt.Errorf("cannot begin transaction: %w", err) } defer func() { rollbackOnTxError(ctx, &tx, err) @@ -219,7 +219,7 @@ func (s *store) connectorsQueryContext(qb query.Builder) (string, []any, error) case key == "name", key == "id": return fmt.Sprintf("%s %s ?", key, query.DefaultComparisonOperatorsMapping[operator]), []any{value}, nil default: - return "", nil, errors.Wrap(ErrValidation, fmt.Sprintf("unknown key '%s' when building query", key)) + return "", nil, fmt.Errorf("unknown key '%s' when building query: %w", key, ErrValidation) } })) } diff --git a/internal/storage/payment_initiation_reversals.go b/internal/storage/payment_initiation_reversals.go index 8b8493b0b..d0c456ed1 100644 --- a/internal/storage/payment_initiation_reversals.go +++ b/internal/storage/payment_initiation_reversals.go @@ -144,7 +144,7 @@ func (s *store) paymentsInitiationReversalQueryContext(qb query.Builder) (string return fmt.Sprintf("%s %s ?", key, query.DefaultComparisonOperatorsMapping[operator]), []any{value}, nil case metadataRegex.Match([]byte(key)): if operator != "$match" { - return "", nil, errors.Wrap(ErrValidation, "'metadata' column can only be used with $match") + return "", nil, fmt.Errorf("'metadata' column can only be used with $match: %w", ErrValidation) } match := metadataRegex.FindAllStringSubmatch(key, 3) @@ -153,7 +153,7 @@ func (s *store) paymentsInitiationReversalQueryContext(qb query.Builder) (string match[0][1]: value, }}, nil default: - return "", nil, errors.Wrap(ErrValidation, fmt.Sprintf("unknown key '%s' when building query", key)) + return "", nil, fmt.Errorf("unknown key '%s' when building query: %w", key, ErrValidation) } })) diff --git a/internal/storage/payment_initiations.go b/internal/storage/payment_initiations.go index 74e0e3d5c..ede5d2230 100644 --- a/internal/storage/payment_initiations.go +++ b/internal/storage/payment_initiations.go @@ -442,12 +442,12 @@ func (s *store) paymentsInitiationAdjustmentsQueryContext(qb query.Builder) (str switch { case key == "status": if operator != "$match" { - return "", nil, errors.Wrap(ErrValidation, "'type' column can only be used with $match") + return "", nil, fmt.Errorf("'type' column can only be used with $match: %w", ErrValidation) } return fmt.Sprintf("%s = ?", key), []any{value}, nil case metadataRegex.Match([]byte(key)): if operator != "$match" { - return "", nil, errors.Wrap(ErrValidation, "'metadata' column can only be used with $match") + return "", nil, fmt.Errorf("'metadata' column can only be used with $match: %w", ErrValidation) } match := metadataRegex.FindAllStringSubmatch(key, 3) key := "metadata" @@ -455,7 +455,7 @@ func (s *store) paymentsInitiationAdjustmentsQueryContext(qb query.Builder) (str match[0][1]: value, }}, nil default: - return "", nil, errors.Wrap(ErrValidation, fmt.Sprintf("unknown key '%s' when building query", key)) + return "", nil, fmt.Errorf("unknown key '%s' when building query: %w", key, ErrValidation) } })) return where, args, err diff --git a/internal/storage/payments.go b/internal/storage/payments.go index 73826f514..34531547e 100644 --- a/internal/storage/payments.go +++ b/internal/storage/payments.go @@ -11,7 +11,6 @@ import ( "github.com/formancehq/go-libs/v3/query" "github.com/formancehq/go-libs/v3/time" "github.com/formancehq/payments/internal/models" - "github.com/pkg/errors" "github.com/uptrace/bun" ) @@ -111,7 +110,7 @@ func (s *store) PaymentsUpsert(ctx context.Context, payments []models.Payment) e tx, err := s.db.BeginTx(ctx, &sql.TxOptions{}) if err != nil { - return errors.Wrap(err, "failed to create transaction") + return fmt.Errorf("failed to create transaction: %w", err) } defer func() { rollbackOnTxError(ctx, &tx, err) @@ -292,7 +291,7 @@ func (s *store) paymentsQueryContext(qb query.Builder) (string, []any, error) { return fmt.Sprintf("%s %s ?", key, query.DefaultComparisonOperatorsMapping[operator]), []any{value}, nil case metadataRegex.Match([]byte(key)): if operator != "$match" { - return "", nil, errors.Wrap(ErrValidation, "'metadata' column can only be used with $match") + return "", nil, fmt.Errorf("'metadata' column can only be used with $match: %w", ErrValidation) } match := metadataRegex.FindAllStringSubmatch(key, 3) @@ -301,7 +300,7 @@ func (s *store) paymentsQueryContext(qb query.Builder) (string, []any, error) { match[0][1]: value, }}, nil default: - return "", nil, errors.Wrap(ErrValidation, fmt.Sprintf("unknown key '%s' when building query", key)) + return "", nil, fmt.Errorf("unknown key '%s' when building query: %w", key, ErrValidation) } })) diff --git a/internal/storage/schedules.go b/internal/storage/schedules.go index ff0f991d2..53d2c8121 100644 --- a/internal/storage/schedules.go +++ b/internal/storage/schedules.go @@ -9,7 +9,6 @@ import ( "github.com/formancehq/go-libs/v3/query" "github.com/formancehq/go-libs/v3/time" "github.com/formancehq/payments/internal/models" - "github.com/pkg/errors" "github.com/uptrace/bun" ) @@ -86,7 +85,7 @@ func (s *store) schedulesQueryContext(qb query.Builder) (string, []any, error) { } return fmt.Sprintf("%s = ?", key), []any{value}, nil default: - return "", nil, errors.Wrap(ErrValidation, fmt.Sprintf("unknown key '%s' when building query", key)) + return "", nil, fmt.Errorf("unknown key '%s' when building query: %w", key, ErrValidation) } })) } diff --git a/internal/storage/workflow_instances.go b/internal/storage/workflow_instances.go index 0873dde89..3c8a3ab5b 100644 --- a/internal/storage/workflow_instances.go +++ b/internal/storage/workflow_instances.go @@ -10,7 +10,6 @@ import ( "github.com/formancehq/go-libs/v3/query" internalTime "github.com/formancehq/go-libs/v3/time" "github.com/formancehq/payments/internal/models" - "github.com/pkg/errors" "github.com/uptrace/bun" ) @@ -87,11 +86,11 @@ func (s *store) instancesQueryContext(qb query.Builder) (string, []any, error) { case key == "schedule_id", key == "connector_id": if operator != "$match" { - return "", nil, errors.Wrap(ErrValidation, "'connector_id' column can only be used with $match") + return "", nil, fmt.Errorf("'connector_id' column can only be used with $match: %w", ErrValidation) } return fmt.Sprintf("%s = ?", key), []any{value}, nil default: - return "", nil, errors.Wrap(ErrValidation, fmt.Sprintf("unknown key '%s' when building query", key)) + return "", nil, fmt.Errorf("unknown key '%s' when building query: %w", key, ErrValidation) } })) } From c479ec411e50498b07393e3917c4adfc63e1117c Mon Sep 17 00:00:00 2001 From: Paul Nicolas Date: Fri, 25 Apr 2025 12:21:32 +0200 Subject: [PATCH 02/13] feat: add psu models --- internal/models/bank_accounts.go | 59 +++++++++++++++---- internal/models/psu.go | 97 ++++++++++++++++++++++++++++++++ 2 files changed, 146 insertions(+), 10 deletions(-) create mode 100644 internal/models/psu.go diff --git a/internal/models/bank_accounts.go b/internal/models/bank_accounts.go index 3e6e82367..cf0411c16 100644 --- a/internal/models/bank_accounts.go +++ b/internal/models/bank_accounts.go @@ -2,25 +2,34 @@ package models import ( "errors" + "fmt" "strings" "time" + "github.com/formancehq/go-libs/v3/pointer" "github.com/google/uuid" ) const ( bankAccountOwnerNamespace = formanceMetadataSpecNamespace + "owner/" + // Bank Account metadata BankAccountOwnerAddressLine1MetadataKey = bankAccountOwnerNamespace + "addressLine1" BankAccountOwnerAddressLine2MetadataKey = bankAccountOwnerNamespace + "addressLine2" + BankAccountOwnerStreetNameMetadataKey = bankAccountOwnerNamespace + "streetName" + BankAccountOwnerStreetNumberMetadataKey = bankAccountOwnerNamespace + "streetNumber" BankAccountOwnerCityMetadataKey = bankAccountOwnerNamespace + "city" BankAccountOwnerRegionMetadataKey = bankAccountOwnerNamespace + "region" BankAccountOwnerPostalCodeMetadataKey = bankAccountOwnerNamespace + "postalCode" - BankAccountAccountNumberMetadataKey = bankAccountOwnerNamespace + "accountNumber" - BankAccountIBANMetadataKey = bankAccountOwnerNamespace + "iban" - BankAccountSwiftBicCodeMetadataKey = bankAccountOwnerNamespace + "swiftBicCode" - BankAccountCountryMetadataKey = bankAccountOwnerNamespace + "country" - BankAccountNameMetadataKey = bankAccountOwnerNamespace + "name" + BankAccountOwnerEmailMetadataKey = bankAccountOwnerNamespace + "email" + BankAccountOwnerPhoneNumberMetadataKey = bankAccountOwnerNamespace + "phoneNumber" + + // Account metadata + AccountIBANMetadataKey = bankAccountOwnerNamespace + "iban" + AccountAccountNumberMetadataKey = bankAccountOwnerNamespace + "accountNumber" + AccountBankAccountNameMetadataKey = bankAccountOwnerNamespace + "name" + AccountBankAccountCountryMetadataKey = bankAccountOwnerNamespace + "country" + AccountSwiftBicCodeMetadataKey = bankAccountOwnerNamespace + "swiftBicCode" ) type BankAccount struct { @@ -89,20 +98,50 @@ func FillBankAccountDetailsToAccountMetadata(account *Account, bankAccount *Bank account.Metadata[BankAccountOwnerPostalCodeMetadataKey] = bankAccount.Metadata[BankAccountOwnerPostalCodeMetadataKey] if bankAccount.AccountNumber != nil { - account.Metadata[BankAccountAccountNumberMetadataKey] = *bankAccount.AccountNumber + account.Metadata[AccountAccountNumberMetadataKey] = *bankAccount.AccountNumber } if bankAccount.IBAN != nil { - account.Metadata[BankAccountIBANMetadataKey] = *bankAccount.IBAN + account.Metadata[AccountIBANMetadataKey] = *bankAccount.IBAN } if bankAccount.SwiftBicCode != nil { - account.Metadata[BankAccountSwiftBicCodeMetadataKey] = *bankAccount.SwiftBicCode + account.Metadata[AccountSwiftBicCodeMetadataKey] = *bankAccount.SwiftBicCode } if bankAccount.Country != nil { - account.Metadata[BankAccountCountryMetadataKey] = *bankAccount.Country + account.Metadata[AccountBankAccountCountryMetadataKey] = *bankAccount.Country } - account.Metadata[BankAccountNameMetadataKey] = bankAccount.Name + account.Metadata[AccountBankAccountNameMetadataKey] = bankAccount.Name +} + +func FillBankAccountMetadataWithPaymentServiceUserInfo(ba *BankAccount, psu *PaymentServiceUser) { + if psu.Address != nil { + var addressLine1 *string + switch { + case psu.Address.StreetNumber != nil && psu.Address.StreetName != nil: + addressLine1 = pointer.For(fmt.Sprintf("%s %s", *psu.Address.StreetNumber, *psu.Address.StreetName)) + case psu.Address.StreetName != nil: + addressLine1 = psu.Address.StreetName + } + + fillMetadata(ba.Metadata, BankAccountOwnerAddressLine1MetadataKey, addressLine1) + fillMetadata(ba.Metadata, BankAccountOwnerStreetNameMetadataKey, psu.Address.StreetName) + fillMetadata(ba.Metadata, BankAccountOwnerStreetNumberMetadataKey, psu.Address.StreetNumber) + fillMetadata(ba.Metadata, BankAccountOwnerCityMetadataKey, psu.Address.City) + fillMetadata(ba.Metadata, BankAccountOwnerRegionMetadataKey, psu.Address.Region) + fillMetadata(ba.Metadata, BankAccountOwnerPostalCodeMetadataKey, psu.Address.PostalCode) + } + + if psu.ContactDetails != nil { + fillMetadata(ba.Metadata, BankAccountOwnerEmailMetadataKey, psu.ContactDetails.Email) + fillMetadata(ba.Metadata, BankAccountOwnerPhoneNumberMetadataKey, psu.ContactDetails.PhoneNumber) + } +} + +func fillMetadata(metadata map[string]string, key string, value *string) { + if value != nil { + metadata[key] = *value + } } diff --git a/internal/models/psu.go b/internal/models/psu.go new file mode 100644 index 000000000..ed4505794 --- /dev/null +++ b/internal/models/psu.go @@ -0,0 +1,97 @@ +package models + +import ( + "encoding/json" + "time" + + "github.com/google/uuid" +) + +type Address struct { + StreetName *string `json:"streetName"` + StreetNumber *string `json:"streetNumber"` + City *string `json:"city"` + Region *string `json:"region"` + PostalCode *string `json:"postalCode"` + Country *string `json:"country"` +} + +type ContactDetails struct { + Email *string `json:"email"` + PhoneNumber *string `json:"phoneNumber"` +} + +type PaymentServiceUser struct { + ID uuid.UUID `json:"id"` + Name string `json:"name"` + CreatedAt time.Time `json:"createdAt"` + + // Optional fields + ContactDetails *ContactDetails `json:"contactDetails"` + Address *Address `json:"address"` + BankAccountIDs []uuid.UUID `json:"bankAccountIDs"` + Metadata map[string]string `json:"metadata"` +} + +func (psu PaymentServiceUser) MarshalJSON() ([]byte, error) { + return json.Marshal(&struct { + ID string `json:"id"` + Name string `json:"name"` + CreatedAt time.Time `json:"createdAt"` + ContactDetails *ContactDetails `json:"contactDetails"` + Address *Address `json:"address"` + BankAccountIDs []string `json:"bankAccountIDs"` + Metadata map[string]string `json:"metadata"` + }{ + ID: psu.ID.String(), + Name: psu.Name, + CreatedAt: psu.CreatedAt, + ContactDetails: psu.ContactDetails, + Address: psu.Address, + BankAccountIDs: func() []string { + if len(psu.BankAccountIDs) == 0 { + return nil + } + bankAccountIDs := make([]string, len(psu.BankAccountIDs)) + for i, id := range psu.BankAccountIDs { + bankAccountIDs[i] = id.String() + } + return bankAccountIDs + }(), + Metadata: psu.Metadata, + }) +} + +func (psu *PaymentServiceUser) UnmarshalJSON(data []byte) error { + var aux struct { + ID string `json:"id"` + Name string `json:"name"` + CreatedAt time.Time `json:"createdAt"` + ContactDetails *ContactDetails `json:"contactDetails"` + Address *Address `json:"address"` + BankAccountIDs []string `json:"bankAccountIDs"` + Metadata map[string]string `json:"metadata"` + } + + if err := json.Unmarshal(data, &aux); err != nil { + return err + } + + psu.ID, _ = uuid.Parse(aux.ID) + psu.Name = aux.Name + psu.CreatedAt = aux.CreatedAt + psu.ContactDetails = aux.ContactDetails + psu.Address = aux.Address + psu.Metadata = aux.Metadata + + if len(aux.BankAccountIDs) > 0 { + psu.BankAccountIDs = make([]uuid.UUID, len(aux.BankAccountIDs)) + for i, id := range aux.BankAccountIDs { + psu.BankAccountIDs[i], _ = uuid.Parse(id) + } + } else { + psu.BankAccountIDs = nil + } + + return nil +} From cfae58cc1afd6c83befe6b4509c6184c338b444b Mon Sep 17 00:00:00 2001 From: Paul Nicolas Date: Fri, 25 Apr 2025 12:21:50 +0200 Subject: [PATCH 03/13] feat(storage): add psu migrations and functions --- .../migrations/14-create-psu-tables.sql | 47 ++ internal/storage/migrations/migrations.go | 14 + internal/storage/psu.go | 320 ++++++++++++++ internal/storage/psu_test.go | 410 ++++++++++++++++++ internal/storage/storage.go | 6 + internal/storage/storage_generated.go | 58 +++ 6 files changed, 855 insertions(+) create mode 100644 internal/storage/migrations/14-create-psu-tables.sql create mode 100644 internal/storage/psu.go create mode 100644 internal/storage/psu_test.go diff --git a/internal/storage/migrations/14-create-psu-tables.sql b/internal/storage/migrations/14-create-psu-tables.sql new file mode 100644 index 000000000..02c4d600f --- /dev/null +++ b/internal/storage/migrations/14-create-psu-tables.sql @@ -0,0 +1,47 @@ +-- payment service users +create table if not exists payment_service_users ( + -- Autoincrement fields + sort_id bigserial not null, + + -- Mandatory fields + id uuid not null, + created_at timestamp without time zone not null, + + -- Encrypted fields + name bytea, + street_name bytea, + street_number bytea, + postal_code bytea, + city bytea, + region bytea, + country bytea, + email bytea, + phone_number bytea, + + -- Optional fields with default + metadata jsonb not null default '{}'::jsonb, + + -- Primary key + primary key (id) +); + +create index psu_created_at_sort_id on payment_service_users (created_at, sort_id); + +create table if not exists psu_bank_accounts ( + -- Mandatory fields + psu_id uuid not null, + bank_account_id uuid not null, + + primary key (psu_id, bank_account_id) +); + +alter table psu_bank_accounts + add constraint fk_psu_bank_accounts_psu_id + foreign key (psu_id) + references payment_service_users (id) + on delete cascade; +alter table psu_bank_accounts + add constraint fk_psu_bank_accounts_bank_account_id + foreign key (bank_account_id) + references bank_accounts (id) + on delete cascade; \ No newline at end of file diff --git a/internal/storage/migrations/migrations.go b/internal/storage/migrations/migrations.go index c190bb05e..53704014b 100644 --- a/internal/storage/migrations/migrations.go +++ b/internal/storage/migrations/migrations.go @@ -25,6 +25,9 @@ var migratePoolsFromV2 string //go:embed 13-connector-providers-lowercase.sql var connectorProvidersLower string +//go:embed 14-create-psu-tables.sql +var psuTableCreation string + func registerMigrations(logger logging.Logger, migrator *migrations.Migrator, encryptionKey string) { migrator.RegisterMigrations( migrations.Migration{ @@ -190,6 +193,17 @@ func registerMigrations(logger logging.Logger, migrator *migrations.Migrator, en }) }, }, + migrations.Migration{ + Name: "create psu tables", + Up: func(ctx context.Context, db bun.IDB) error { + return db.RunInTx(ctx, &sql.TxOptions{}, func(ctx context.Context, tx bun.Tx) error { + logger.Info("running psu table creations migration...") + _, err := tx.ExecContext(ctx, psuTableCreation) + logger.WithField("error", err).Info("finished running psu table creations migration") + return err + }) + }, + }, ) } diff --git a/internal/storage/psu.go b/internal/storage/psu.go new file mode 100644 index 000000000..9943977a0 --- /dev/null +++ b/internal/storage/psu.go @@ -0,0 +1,320 @@ +package storage + +import ( + "context" + "fmt" + + "github.com/formancehq/go-libs/v3/bun/bunpaginate" + "github.com/formancehq/go-libs/v3/query" + "github.com/formancehq/go-libs/v3/time" + "github.com/formancehq/payments/internal/models" + "github.com/google/uuid" + "github.com/uptrace/bun" +) + +type paymentServiceUser struct { + bun.BaseModel `bun:"payment_service_users"` + + // Mandatory fields + ID uuid.UUID `bun:"id,pk,type:uuid,notnull"` + CreatedAt time.Time `bun:"created_at,type:timestamp without time zone,notnull"` + + // Encrypted fields + Name string `bun:"decrypted_name,scanonly"` + StreetName *string `bun:"decrypted_street_name,scanonly"` + StreetNumber *string `bun:"decrypted_street_number,scanonly"` + City *string `bun:"decrypted_city,scanonly"` + PostalCode *string `bun:"decrypted_postal_code,scanonly"` + Region *string `bun:"decrypted_region,scanonly"` + Country *string `bun:"decrypted_country,scanonly"` + Email *string `bun:"decrypted_email,scanonly"` + PhoneNumber *string `bun:"decrypted_phone,scanonly"` + + // Optional fields with default + Metadata map[string]string `bun:"metadata,type:jsonb,nullzero,notnull,default:'{}'"` + + // Relations + RelatedBankAccounts []psuBankAccounts `bun:"rel:has-many,join:id=psu_id"` +} + +type psuBankAccounts struct { + bun.BaseModel `bun:"psu_bank_accounts"` + + // Mandatory fields + PsuID uuid.UUID `bun:"psu_id,pk,type:uuid,notnull"` + BankAccountID uuid.UUID `bun:"bank_account_id,pk,type:uuid,notnull"` +} + +func (s *store) PaymentServiceUsersCreate(ctx context.Context, psu models.PaymentServiceUser) error { + paymentServiceUser, relatedBankAccounts := fromPaymentServiceUserModels(psu) + + tx, err := s.db.BeginTx(ctx, nil) + if err != nil { + return e("begin transaction: %w", err) + } + + var errTx error + defer func() { + if errTx != nil { + if err := tx.Rollback(); err != nil { + s.logger.Errorf("failed to rollback transaction: %v", err) + } + } + }() + + _, err = tx.NewRaw(` + INSERT INTO payment_service_users (id, created_at, metadata, name, street_name, street_number, city, region, postal_code, country, email, phone_number) + VALUES (?0, ?1, ?2, + pgp_sym_encrypt(?3::TEXT, ?12, ?13), + pgp_sym_encrypt(?4::TEXT, ?12, ?13), + pgp_sym_encrypt(?5::TEXT, ?12, ?13), + pgp_sym_encrypt(?6::TEXT, ?12, ?13), + pgp_sym_encrypt(?7::TEXT, ?12, ?13), + pgp_sym_encrypt(?8::TEXT, ?12, ?13), + pgp_sym_encrypt(?9::TEXT, ?12, ?13), + pgp_sym_encrypt(?10::TEXT, ?12, ?13), + pgp_sym_encrypt(?11::TEXT, ?12, ?13) + ) + ON CONFLICT (id) DO NOTHING + RETURNING id + `, paymentServiceUser.ID, paymentServiceUser.CreatedAt, paymentServiceUser.Metadata, + paymentServiceUser.Name, paymentServiceUser.StreetName, paymentServiceUser.StreetNumber, paymentServiceUser.City, + paymentServiceUser.Region, paymentServiceUser.PostalCode, paymentServiceUser.Country, paymentServiceUser.Email, + paymentServiceUser.PhoneNumber, s.configEncryptionKey, encryptionOptions, + ).Exec(ctx) + if err != nil { + errTx = err + return e("insert psu: %w", err) + } + + if len(relatedBankAccounts) > 0 { + // Insert or update the related accounts + _, err = tx.NewInsert(). + Model(&relatedBankAccounts). + On("CONFLICT (psu_id, bank_account_id) DO NOTHING"). + Exec(ctx) + if err != nil { + errTx = err + return e("insert related bank accounts", err) + } + } + + if err := tx.Commit(); err != nil { + errTx = err + return e("commit transaction", tx.Commit()) + } + + return nil +} + +func (s *store) PaymentServiceUsersGet(ctx context.Context, id uuid.UUID) (*models.PaymentServiceUser, error) { + var psu paymentServiceUser + query := s.db.NewSelect(). + Model(&psu). + Column("id", "created_at", "metadata"). + Where("id = ?", id). + Relation("RelatedBankAccounts") + + query = s.paymentServiceUsersSelectDecryptColumnExpr(query) + + err := query. + Scan(ctx) + if err != nil { + return nil, e("select psu: %w", err) + } + + res := toPaymentServiceUserModels(psu) + + return &res, nil +} + +type PSUQuery struct{} + +type ListPSUsQuery bunpaginate.OffsetPaginatedQuery[bunpaginate.PaginatedQueryOptions[PSUQuery]] + +func NewListPSUQuery(opts bunpaginate.PaginatedQueryOptions[PSUQuery]) ListPSUsQuery { + return ListPSUsQuery{ + Order: bunpaginate.OrderAsc, + PageSize: opts.PageSize, + Options: opts, + } +} + +func (s *store) paymentServiceUsersQueryContext(qb query.Builder) (string, []any, error) { + return qb.Build(query.ContextFn(func(key, operator string, value any) (string, []any, error) { + switch { + case key == "id": + if operator != "$match" { + return "", nil, fmt.Errorf("'%s' column can only be used with $match: %w", key, ErrValidation) + } + return fmt.Sprintf("%s = ?", key), []any{value}, nil + case metadataRegex.Match([]byte(key)): + if operator != "$match" { + return "", nil, fmt.Errorf("'metadata' column can only be used with $match: %w", ErrValidation) + } + match := metadataRegex.FindAllStringSubmatch(key, 3) + + key := "metadata" + return key + " @> ?", []any{map[string]any{ + match[0][1]: value, + }}, nil + default: + return "", nil, fmt.Errorf("unknown key '%s' when building query: %w", key, ErrValidation) + } + })) +} + +func (s *store) PaymentServiceUsersList(ctx context.Context, query ListPSUsQuery) (*bunpaginate.Cursor[models.PaymentServiceUser], error) { + var ( + where string + args []any + err error + ) + if query.Options.QueryBuilder != nil { + where, args, err = s.paymentServiceUsersQueryContext(query.Options.QueryBuilder) + if err != nil { + return nil, err + } + } + + cursor, err := paginateWithOffset[bunpaginate.PaginatedQueryOptions[PSUQuery], paymentServiceUser](s, ctx, + (*bunpaginate.OffsetPaginatedQuery[bunpaginate.PaginatedQueryOptions[PSUQuery]])(&query), + func(query *bun.SelectQuery) *bun.SelectQuery { + query = query.Relation("RelatedBankAccounts") + query = query.Column("id", "created_at", "metadata") + query = s.paymentServiceUsersSelectDecryptColumnExpr(query) + + if where != "" { + query = query.Where(where, args...) + } + + query = query.Order("created_at DESC", "sort_id DESC") + + return query + }, + ) + if err != nil { + return nil, e("failed to fetch accounts", err) + } + + counterParties := make([]models.PaymentServiceUser, 0, len(cursor.Data)) + for _, a := range cursor.Data { + counterParties = append(counterParties, toPaymentServiceUserModels(a)) + } + + return &bunpaginate.Cursor[models.PaymentServiceUser]{ + PageSize: cursor.PageSize, + HasMore: cursor.HasMore, + Previous: cursor.Previous, + Next: cursor.Next, + Data: counterParties, + }, nil +} + +func (s *store) PaymentServiceUsersAddBankAccount(ctx context.Context, psuID, bankAccountID uuid.UUID) error { + toInsert := psuBankAccounts{ + PsuID: psuID, + BankAccountID: bankAccountID, + } + + _, err := s.db.NewInsert(). + Model(&toInsert). + On("CONFLICT (psu_id, bank_account_id) DO NOTHING"). + Exec(ctx) + if err != nil { + return e("insert related bank account: %w", err) + } + + return nil +} + +func (s *store) paymentServiceUsersSelectDecryptColumnExpr(query *bun.SelectQuery) *bun.SelectQuery { + return query. + ColumnExpr("pgp_sym_decrypt(name, ?, ?) as decrypted_name", s.configEncryptionKey, encryptionOptions). + ColumnExpr("pgp_sym_decrypt(street_name, ?, ?) as decrypted_street_name", s.configEncryptionKey, encryptionOptions). + ColumnExpr("pgp_sym_decrypt(street_number, ?, ?) as decrypted_street_number", s.configEncryptionKey, encryptionOptions). + ColumnExpr("pgp_sym_decrypt(city, ?, ?) as decrypted_city", s.configEncryptionKey, encryptionOptions). + ColumnExpr("pgp_sym_decrypt(region, ?, ?) as decrypted_region", s.configEncryptionKey, encryptionOptions). + ColumnExpr("pgp_sym_decrypt(postal_code, ?, ?) as decrypted_postal_code", s.configEncryptionKey, encryptionOptions). + ColumnExpr("pgp_sym_decrypt(country, ?, ?) as decrypted_country", s.configEncryptionKey, encryptionOptions). + ColumnExpr("pgp_sym_decrypt(email, ?, ?) as decrypted_email", s.configEncryptionKey, encryptionOptions). + ColumnExpr("pgp_sym_decrypt(phone_number, ?, ?) as decrypted_phone", s.configEncryptionKey, encryptionOptions) +} + +func fromPaymentServiceUserModels(from models.PaymentServiceUser) (paymentServiceUser, []psuBankAccounts) { + psu := paymentServiceUser{ + ID: from.ID, + CreatedAt: time.New(from.CreatedAt), + Name: from.Name, + Metadata: from.Metadata, + } + + bankAccounts := make([]psuBankAccounts, len(from.BankAccountIDs)) + for i, id := range from.BankAccountIDs { + bankAccounts[i] = psuBankAccounts{ + PsuID: from.ID, + BankAccountID: id, + } + } + + if from.Address != nil { + psu.StreetName = from.Address.StreetName + psu.StreetNumber = from.Address.StreetNumber + psu.City = from.Address.City + psu.PostalCode = from.Address.PostalCode + psu.Region = from.Address.Region + psu.Country = from.Address.Country + } + + if from.ContactDetails != nil { + psu.Email = from.ContactDetails.Email + psu.PhoneNumber = from.ContactDetails.PhoneNumber + } + + return psu, bankAccounts +} + +func toPaymentServiceUserModels(from paymentServiceUser) models.PaymentServiceUser { + psu := models.PaymentServiceUser{ + ID: from.ID, + CreatedAt: from.CreatedAt.Time, + Name: from.Name, + Metadata: from.Metadata, + } + + psu.Address = fillAddress(from) + psu.ContactDetails = fillContactDetails(from) + + psu.BankAccountIDs = make([]uuid.UUID, len(from.RelatedBankAccounts)) + for i, bankAccount := range from.RelatedBankAccounts { + psu.BankAccountIDs[i] = bankAccount.BankAccountID + } + + return psu +} + +func fillAddress(from paymentServiceUser) *models.Address { + if from.StreetName == nil && from.StreetNumber == nil && from.City == nil && from.PostalCode == nil && from.Region == nil && from.Country == nil { + return nil + } + + return &models.Address{ + StreetName: from.StreetName, + StreetNumber: from.StreetNumber, + City: from.City, + PostalCode: from.PostalCode, + Region: from.Region, + Country: from.Country, + } +} + +func fillContactDetails(from paymentServiceUser) *models.ContactDetails { + if from.Email == nil && from.PhoneNumber == nil { + return nil + } + + return &models.ContactDetails{ + Email: from.Email, + PhoneNumber: from.PhoneNumber, + } +} diff --git a/internal/storage/psu_test.go b/internal/storage/psu_test.go new file mode 100644 index 000000000..7260078d1 --- /dev/null +++ b/internal/storage/psu_test.go @@ -0,0 +1,410 @@ +package storage + +import ( + "context" + "testing" + + "github.com/formancehq/go-libs/v3/bun/bunpaginate" + "github.com/formancehq/go-libs/v3/logging" + "github.com/formancehq/go-libs/v3/pointer" + "github.com/formancehq/go-libs/v3/query" + "github.com/formancehq/go-libs/v3/time" + "github.com/formancehq/payments/internal/models" + "github.com/google/uuid" + "github.com/stretchr/testify/require" +) + +var ( + defaultPSU = models.PaymentServiceUser{ + ID: uuid.New(), + Name: "test", + CreatedAt: now.Add(-60 * time.Minute).UTC().Time, + ContactDetails: &models.ContactDetails{ + Email: pointer.For("test"), + PhoneNumber: pointer.For("test"), + }, + Address: &models.Address{ + StreetName: pointer.For("test"), + StreetNumber: pointer.For("test"), + City: pointer.For("test"), + Region: pointer.For("test"), + PostalCode: pointer.For("test"), + Country: pointer.For("test"), + }, + BankAccountIDs: []uuid.UUID{defaultBankAccount.ID}, + Metadata: map[string]string{ + "foo": "bar", + }, + } + + defaultPSU2 = models.PaymentServiceUser{ + ID: uuid.New(), + Name: "test2", + CreatedAt: now.Add(-30 * time.Minute).UTC().Time, + } + + defaultPSU3 = models.PaymentServiceUser{ + ID: uuid.New(), + Name: "test", + CreatedAt: now.Add(-55 * time.Minute).UTC().Time, + ContactDetails: &models.ContactDetails{ + Email: pointer.For("test"), + }, + Address: &models.Address{ + StreetName: pointer.For("test"), + PostalCode: pointer.For("test"), + Country: pointer.For("test"), + }, + BankAccountIDs: []uuid.UUID{defaultBankAccount.ID}, + } +) + +func createPSU(t *testing.T, ctx context.Context, storage Storage, psu models.PaymentServiceUser) { + require.NoError(t, storage.PaymentServiceUsersCreate(ctx, psu)) +} + +func TestPSUCreate(t *testing.T) { + t.Parallel() + + ctx := logging.TestingContext() + store := newStore(t) + + upsertConnector(t, ctx, store, defaultConnector) + upsertBankAccount(t, ctx, store, defaultBankAccount) + upsertAccounts(t, ctx, store, defaultAccounts()) + createPSU(t, ctx, store, defaultPSU) + createPSU(t, ctx, store, defaultPSU2) + createPSU(t, ctx, store, defaultPSU3) + + t.Run("upsert with same id", func(t *testing.T) { + psu := models.PaymentServiceUser{ + ID: defaultPSU.ID, + Name: "changed", + CreatedAt: now.Add(-40 * time.Minute).UTC().Time, + ContactDetails: &models.ContactDetails{ + Email: pointer.For("changed"), + PhoneNumber: pointer.For("changed"), + }, + Address: &models.Address{ + StreetName: pointer.For("changed"), + StreetNumber: pointer.For("changed"), + City: pointer.For("changed"), + Region: pointer.For("changed"), + PostalCode: pointer.For("changed"), + Country: pointer.For("changed"), + }, + } + + require.NoError(t, store.PaymentServiceUsersCreate(ctx, psu)) + + actual, err := store.PaymentServiceUsersGet(ctx, defaultPSU.ID) + require.NoError(t, err) + // Should not update the counter party + comparePSUs(t, defaultPSU, *actual) + }) + + t.Run("unknown psu id", func(t *testing.T) { + cp := models.PaymentServiceUser{ + ID: uuid.New(), + Name: "test", + CreatedAt: now.Add(-60 * time.Minute).UTC().Time, + ContactDetails: &models.ContactDetails{}, + Address: &models.Address{}, + BankAccountIDs: []uuid.UUID{uuid.New()}, + } + + require.Error(t, store.PaymentServiceUsersCreate(ctx, cp)) + }) +} + +func TestPSUGet(t *testing.T) { + t.Parallel() + + ctx := logging.TestingContext() + store := newStore(t) + upsertConnector(t, ctx, store, defaultConnector) + upsertBankAccount(t, ctx, store, defaultBankAccount) + upsertAccounts(t, ctx, store, defaultAccounts()) + createPSU(t, ctx, store, defaultPSU) + createPSU(t, ctx, store, defaultPSU2) + createPSU(t, ctx, store, defaultPSU3) + + t.Run("get psu will all fields filled", func(t *testing.T) { + actual, err := store.PaymentServiceUsersGet(ctx, defaultPSU.ID) + require.NoError(t, err) + comparePSUs(t, defaultPSU, *actual) + }) + + t.Run("get psu with only required fields", func(t *testing.T) { + actual, err := store.PaymentServiceUsersGet(ctx, defaultPSU2.ID) + require.NoError(t, err) + comparePSUs(t, defaultPSU2, *actual) + }) + + t.Run("get psu with only required fields and some optional fields", func(t *testing.T) { + actual, err := store.PaymentServiceUsersGet(ctx, defaultPSU3.ID) + require.NoError(t, err) + comparePSUs(t, defaultPSU3, *actual) + }) +} + +func TestPSUList(t *testing.T) { + t.Parallel() + + ctx := logging.TestingContext() + store := newStore(t) + upsertConnector(t, ctx, store, defaultConnector) + upsertBankAccount(t, ctx, store, defaultBankAccount) + upsertAccounts(t, ctx, store, defaultAccounts()) + createPSU(t, ctx, store, defaultPSU) + createPSU(t, ctx, store, defaultPSU2) + createPSU(t, ctx, store, defaultPSU3) + + t.Run("wrong query builder when listing by id", func(t *testing.T) { + q := NewListPSUQuery( + bunpaginate.NewPaginatedQueryOptions(PSUQuery{}). + WithPageSize(15). + WithQueryBuilder(query.Lt("id", "test1")), + ) + + cursor, err := store.PaymentServiceUsersList(ctx, q) + require.Error(t, err) + require.Nil(t, cursor) + }) + + t.Run("list psu by id", func(t *testing.T) { + q := NewListPSUQuery( + bunpaginate.NewPaginatedQueryOptions(PSUQuery{}). + WithPageSize(15). + WithQueryBuilder(query.Match("id", defaultPSU.ID.String())), + ) + + cursor, err := store.PaymentServiceUsersList(ctx, q) + require.NoError(t, err) + require.Len(t, cursor.Data, 1) + require.False(t, cursor.HasMore) + require.Empty(t, cursor.Previous) + require.Empty(t, cursor.Next) + comparePSUs(t, defaultPSU, cursor.Data[0]) + }) + + t.Run("list psu by unknown id", func(t *testing.T) { + q := NewListPSUQuery( + bunpaginate.NewPaginatedQueryOptions(PSUQuery{}). + WithPageSize(15). + WithQueryBuilder(query.Match("id", uuid.New())), + ) + + cursor, err := store.PaymentServiceUsersList(ctx, q) + require.NoError(t, err) + require.Empty(t, cursor.Data) + require.False(t, cursor.HasMore) + require.Empty(t, cursor.Previous) + require.Empty(t, cursor.Next) + }) + + t.Run("lsit psu by metadata", func(t *testing.T) { + q := NewListPSUQuery( + bunpaginate.NewPaginatedQueryOptions(PSUQuery{}). + WithPageSize(15). + WithQueryBuilder(query.Match("metadata[foo]", "bar")), + ) + + cursor, err := store.PaymentServiceUsersList(ctx, q) + require.NoError(t, err) + require.Len(t, cursor.Data, 1) + require.False(t, cursor.HasMore) + require.Empty(t, cursor.Previous) + require.Empty(t, cursor.Next) + comparePSUs(t, defaultPSU, cursor.Data[0]) + }) + + t.Run("lsit psu by unknown metadata", func(t *testing.T) { + q := NewListPSUQuery( + bunpaginate.NewPaginatedQueryOptions(PSUQuery{}). + WithPageSize(15). + WithQueryBuilder(query.Match("metadata[unknown]", "bar")), + ) + + cursor, err := store.PaymentServiceUsersList(ctx, q) + require.NoError(t, err) + require.Len(t, cursor.Data, 0) + require.False(t, cursor.HasMore) + require.Empty(t, cursor.Previous) + require.Empty(t, cursor.Next) + }) + + t.Run("lsit psu by metadata", func(t *testing.T) { + q := NewListPSUQuery( + bunpaginate.NewPaginatedQueryOptions(PSUQuery{}). + WithPageSize(15). + WithQueryBuilder(query.Match("metadata[foo]", "bar")), + ) + + cursor, err := store.PaymentServiceUsersList(ctx, q) + require.NoError(t, err) + require.Len(t, cursor.Data, 1) + require.False(t, cursor.HasMore) + require.Empty(t, cursor.Previous) + require.Empty(t, cursor.Next) + comparePSUs(t, defaultPSU, cursor.Data[0]) + }) + + t.Run("list psu test cursor", func(t *testing.T) { + q := NewListPSUQuery( + bunpaginate.NewPaginatedQueryOptions(PSUQuery{}). + WithPageSize(1), + ) + + cursor, err := store.PaymentServiceUsersList(ctx, q) + require.NoError(t, err) + require.Len(t, cursor.Data, 1) + require.True(t, cursor.HasMore) + require.Empty(t, cursor.Previous) + require.NotEmpty(t, cursor.Next) + comparePSUs(t, defaultPSU2, cursor.Data[0]) + + err = bunpaginate.UnmarshalCursor(cursor.Next, &q) + require.NoError(t, err) + cursor, err = store.PaymentServiceUsersList(ctx, q) + require.NoError(t, err) + require.Len(t, cursor.Data, 1) + require.True(t, cursor.HasMore) + require.NotEmpty(t, cursor.Previous) + require.NotEmpty(t, cursor.Next) + comparePSUs(t, defaultPSU3, cursor.Data[0]) + + err = bunpaginate.UnmarshalCursor(cursor.Next, &q) + require.NoError(t, err) + cursor, err = store.PaymentServiceUsersList(ctx, q) + require.NoError(t, err) + require.Len(t, cursor.Data, 1) + require.False(t, cursor.HasMore) + require.NotEmpty(t, cursor.Previous) + require.Empty(t, cursor.Next) + comparePSUs(t, defaultPSU, cursor.Data[0]) + + err = bunpaginate.UnmarshalCursor(cursor.Previous, &q) + require.NoError(t, err) + cursor, err = store.PaymentServiceUsersList(ctx, q) + require.NoError(t, err) + require.Len(t, cursor.Data, 1) + require.True(t, cursor.HasMore) + require.NotEmpty(t, cursor.Previous) + require.NotEmpty(t, cursor.Next) + comparePSUs(t, defaultPSU3, cursor.Data[0]) + + err = bunpaginate.UnmarshalCursor(cursor.Previous, &q) + require.NoError(t, err) + cursor, err = store.PaymentServiceUsersList(ctx, q) + require.NoError(t, err) + require.Len(t, cursor.Data, 1) + require.True(t, cursor.HasMore) + require.Empty(t, cursor.Previous) + require.NotEmpty(t, cursor.Next) + comparePSUs(t, defaultPSU2, cursor.Data[0]) + }) +} + +func TestPSUAddBankAccount(t *testing.T) { + t.Parallel() + + ctx := logging.TestingContext() + store := newStore(t) + upsertConnector(t, ctx, store, defaultConnector) + upsertBankAccount(t, ctx, store, defaultBankAccount) + upsertBankAccount(t, ctx, store, defaultBankAccount2) + upsertAccounts(t, ctx, store, defaultAccounts()) + createPSU(t, ctx, store, defaultPSU) + createPSU(t, ctx, store, defaultPSU2) + createPSU(t, ctx, store, defaultPSU3) + + t.Run("add bank account to psu", func(t *testing.T) { + err := store.PaymentServiceUsersAddBankAccount(ctx, defaultPSU.ID, defaultBankAccount2.ID) + require.NoError(t, err) + + actual, err := store.PaymentServiceUsersGet(ctx, defaultPSU.ID) + require.NoError(t, err) + require.Len(t, actual.BankAccountIDs, 2) + require.Equal(t, defaultBankAccount.ID, actual.BankAccountIDs[0]) + require.Equal(t, defaultBankAccount2.ID, actual.BankAccountIDs[1]) + }) + + t.Run("add unknown account to psu", func(t *testing.T) { + err := store.PaymentServiceUsersAddBankAccount(ctx, defaultPSU.ID, uuid.New()) + require.Error(t, err) + + actual, err := store.PaymentServiceUsersGet(ctx, defaultPSU.ID) + require.NoError(t, err) + require.Len(t, actual.BankAccountIDs, 2) + require.Equal(t, defaultBankAccount.ID, actual.BankAccountIDs[0]) + require.Equal(t, defaultBankAccount2.ID, actual.BankAccountIDs[1]) + }) +} + +func comparePSUs(t *testing.T, expected, actual models.PaymentServiceUser) { + require.Equal(t, expected.ID, actual.ID) + require.Equal(t, expected.CreatedAt, actual.CreatedAt) + require.Equal(t, expected.Name, actual.Name) + + require.Equal(t, len(expected.Metadata), len(actual.Metadata)) + for k, v := range expected.Metadata { + require.Equal(t, v, actual.Metadata[k]) + } + for k, v := range actual.Metadata { + require.Equal(t, v, expected.Metadata[k]) + } + + require.Equal(t, len(expected.BankAccountIDs), len(actual.BankAccountIDs)) + for i := range expected.BankAccountIDs { + require.Equal(t, expected.BankAccountIDs[i], actual.BankAccountIDs[i]) + } + + compareCounterPartiesAddressed(t, expected.Address, actual.Address) + compareCounterPartiesContactDetails(t, expected.ContactDetails, actual.ContactDetails) +} + +func compareCounterPartiesAddressed(t *testing.T, expected, actual *models.Address) { + switch { + case expected == nil && actual == nil: + return + case expected != nil && actual != nil: + // Do the next tests + default: + require.Fail(t, "Address is different") + } + + compareInterface(t, "StreetName", expected.StreetName, actual.StreetName) + compareInterface(t, "StreetNumber", expected.StreetNumber, actual.StreetNumber) + compareInterface(t, "City", expected.City, actual.City) + compareInterface(t, "Region", expected.Region, actual.Region) + compareInterface(t, "PostalCode", expected.PostalCode, actual.PostalCode) + compareInterface(t, "Country", expected.Country, actual.Country) +} + +func compareCounterPartiesContactDetails(t *testing.T, expected, actual *models.ContactDetails) { + switch { + case expected == nil && actual == nil: + return + case expected != nil && actual != nil: + // Do the next tests + default: + require.Fail(t, "ContactDetails is different") + } + + compareInterface(t, "Email", expected.Email, actual.Email) + compareInterface(t, "Phone", expected.PhoneNumber, actual.PhoneNumber) +} + +func compareInterface(t *testing.T, name string, expected, actual interface{}) { + switch { + case expected == nil && actual == nil: + return + case expected != nil && actual != nil: + // Do the next tests + default: + require.Failf(t, "%s field is different", name) + } + + require.Equal(t, expected, actual) +} diff --git a/internal/storage/storage.go b/internal/storage/storage.go index 90b801d21..3b5af74c2 100644 --- a/internal/storage/storage.go +++ b/internal/storage/storage.go @@ -94,6 +94,12 @@ type Storage interface { PaymentInitiationReversalAdjustmentsGet(ctx context.Context, id models.PaymentInitiationReversalAdjustmentID) (*models.PaymentInitiationReversalAdjustment, error) PaymentInitiationReversalAdjustmentsList(ctx context.Context, piID models.PaymentInitiationReversalID, q ListPaymentInitiationReversalAdjustmentsQuery) (*bunpaginate.Cursor[models.PaymentInitiationReversalAdjustment], error) + // Payment Service Users + PaymentServiceUsersCreate(ctx context.Context, psu models.PaymentServiceUser) error + PaymentServiceUsersGet(ctx context.Context, id uuid.UUID) (*models.PaymentServiceUser, error) + PaymentServiceUsersList(ctx context.Context, query ListPSUsQuery) (*bunpaginate.Cursor[models.PaymentServiceUser], error) + PaymentServiceUsersAddBankAccount(ctx context.Context, psuID, bankAccountID uuid.UUID) error + // Pools PoolsUpsert(ctx context.Context, pool models.Pool) error PoolsGet(ctx context.Context, id uuid.UUID) (*models.Pool, error) diff --git a/internal/storage/storage_generated.go b/internal/storage/storage_generated.go index 4bcf1c803..0f6936e38 100644 --- a/internal/storage/storage_generated.go +++ b/internal/storage/storage_generated.go @@ -829,6 +829,64 @@ func (mr *MockStorageMockRecorder) PaymentInitiationsUpdateMetadata(ctx, piID, m return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PaymentInitiationsUpdateMetadata", reflect.TypeOf((*MockStorage)(nil).PaymentInitiationsUpdateMetadata), ctx, piID, metadata) } +// PaymentServiceUsersAddBankAccount mocks base method. +func (m *MockStorage) PaymentServiceUsersAddBankAccount(ctx context.Context, psuID, bankAccountID uuid.UUID) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "PaymentServiceUsersAddBankAccount", ctx, psuID, bankAccountID) + ret0, _ := ret[0].(error) + return ret0 +} + +// PaymentServiceUsersAddBankAccount indicates an expected call of PaymentServiceUsersAddBankAccount. +func (mr *MockStorageMockRecorder) PaymentServiceUsersAddBankAccount(ctx, psuID, bankAccountID any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PaymentServiceUsersAddBankAccount", reflect.TypeOf((*MockStorage)(nil).PaymentServiceUsersAddBankAccount), ctx, psuID, bankAccountID) +} + +// PaymentServiceUsersCreate mocks base method. +func (m *MockStorage) PaymentServiceUsersCreate(ctx context.Context, psu models.PaymentServiceUser) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "PaymentServiceUsersCreate", ctx, psu) + ret0, _ := ret[0].(error) + return ret0 +} + +// PaymentServiceUsersCreate indicates an expected call of PaymentServiceUsersCreate. +func (mr *MockStorageMockRecorder) PaymentServiceUsersCreate(ctx, psu any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PaymentServiceUsersCreate", reflect.TypeOf((*MockStorage)(nil).PaymentServiceUsersCreate), ctx, psu) +} + +// PaymentServiceUsersGet mocks base method. +func (m *MockStorage) PaymentServiceUsersGet(ctx context.Context, id uuid.UUID) (*models.PaymentServiceUser, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "PaymentServiceUsersGet", ctx, id) + ret0, _ := ret[0].(*models.PaymentServiceUser) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// PaymentServiceUsersGet indicates an expected call of PaymentServiceUsersGet. +func (mr *MockStorageMockRecorder) PaymentServiceUsersGet(ctx, id any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PaymentServiceUsersGet", reflect.TypeOf((*MockStorage)(nil).PaymentServiceUsersGet), ctx, id) +} + +// PaymentServiceUsersList mocks base method. +func (m *MockStorage) PaymentServiceUsersList(ctx context.Context, query ListPSUsQuery) (*bunpaginate.Cursor[models.PaymentServiceUser], error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "PaymentServiceUsersList", ctx, query) + ret0, _ := ret[0].(*bunpaginate.Cursor[models.PaymentServiceUser]) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// PaymentServiceUsersList indicates an expected call of PaymentServiceUsersList. +func (mr *MockStorageMockRecorder) PaymentServiceUsersList(ctx, query any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PaymentServiceUsersList", reflect.TypeOf((*MockStorage)(nil).PaymentServiceUsersList), ctx, query) +} + // PaymentsDeleteFromConnectorID mocks base method. func (m *MockStorage) PaymentsDeleteFromConnectorID(ctx context.Context, connectorID models.ConnectorID) error { m.ctrl.T.Helper() From 181300489781f46f2de8a473cf98994f415ab033 Mon Sep 17 00:00:00 2001 From: Paul Nicolas Date: Fri, 25 Apr 2025 12:22:25 +0200 Subject: [PATCH 04/13] feat(*): add payment service users entity --- internal/api/backend/backend.go | 7 + internal/api/backend/backend_generated.go | 73 +++++++++++ .../bank_accounts_forward_to_connector.go | 12 +- ...bank_accounts_forward_to_connector_test.go | 73 +++++++---- .../payment_service_users_add_bank_account.go | 11 ++ ...ent_service_users_add_bank_account_test.go | 59 +++++++++ .../services/payment_service_users_create.go | 11 ++ .../payment_service_users_create_test.go | 58 ++++++++ ...ment_service_users_forward_bank_account.go | 35 +++++ ...service_users_forward_bank_account_test.go | 123 +++++++++++++++++ .../api/services/payment_service_users_get.go | 17 +++ .../payment_service_users_get_test.go | 62 +++++++++ .../services/payment_service_users_list.go | 18 +++ .../payment_service_users_list_test.go | 58 ++++++++ internal/api/v3/errors.go | 4 +- ..._payment_service_users_add_bank_account.go | 43 ++++++ ...ent_service_users_add_bank_account_test.go | 64 +++++++++ .../handler_payment_service_users_create.go | 124 ++++++++++++++++++ ...ndler_payment_service_users_create_test.go | 101 ++++++++++++++ ...users_forward_bank_account_to_connector.go | 75 +++++++++++ ..._forward_bank_account_to_connector_test.go | 85 ++++++++++++ .../v3/handler_payment_service_users_get.go | 35 +++++ .../handler_payment_service_users_get_test.go | 63 +++++++++ .../v3/handler_payment_service_users_list.go | 41 ++++++ ...handler_payment_service_users_list_test.go | 52 ++++++++ internal/api/v3/router.go | 17 +++ .../connectors/engine/activities/activity.go | 4 + .../storage_payment_service_users_get.go | 25 ++++ internal/connectors/engine/engine.go | 20 +-- .../connectors/engine/engine_generated.go | 8 +- internal/connectors/engine/engine_test.go | 38 +----- .../engine/workflow/create_bank_account.go | 22 +--- .../workflow/create_bank_account_test.go | 54 ++------ .../connectors/engine/workflow/main_test.go | 25 ++++ .../plugins/public/bankingcircle/payouts.go | 12 +- .../public/bankingcircle/payouts_test.go | 8 +- 36 files changed, 1394 insertions(+), 143 deletions(-) create mode 100644 internal/api/services/payment_service_users_add_bank_account.go create mode 100644 internal/api/services/payment_service_users_add_bank_account_test.go create mode 100644 internal/api/services/payment_service_users_create.go create mode 100644 internal/api/services/payment_service_users_create_test.go create mode 100644 internal/api/services/payment_service_users_forward_bank_account.go create mode 100644 internal/api/services/payment_service_users_forward_bank_account_test.go create mode 100644 internal/api/services/payment_service_users_get.go create mode 100644 internal/api/services/payment_service_users_get_test.go create mode 100644 internal/api/services/payment_service_users_list.go create mode 100644 internal/api/services/payment_service_users_list_test.go create mode 100644 internal/api/v3/handler_payment_service_users_add_bank_account.go create mode 100644 internal/api/v3/handler_payment_service_users_add_bank_account_test.go create mode 100644 internal/api/v3/handler_payment_service_users_create.go create mode 100644 internal/api/v3/handler_payment_service_users_create_test.go create mode 100644 internal/api/v3/handler_payment_service_users_forward_bank_account_to_connector.go create mode 100644 internal/api/v3/handler_payment_service_users_forward_bank_account_to_connector_test.go create mode 100644 internal/api/v3/handler_payment_service_users_get.go create mode 100644 internal/api/v3/handler_payment_service_users_get_test.go create mode 100644 internal/api/v3/handler_payment_service_users_list.go create mode 100644 internal/api/v3/handler_payment_service_users_list_test.go create mode 100644 internal/connectors/engine/activities/storage_payment_service_users_get.go diff --git a/internal/api/backend/backend.go b/internal/api/backend/backend.go index e62ef5d9a..28dff3141 100644 --- a/internal/api/backend/backend.go +++ b/internal/api/backend/backend.go @@ -66,6 +66,13 @@ type Backend interface { PaymentInitiationRelatedPaymentsList(ctx context.Context, id models.PaymentInitiationID, query storage.ListPaymentInitiationRelatedPaymentsQuery) (*bunpaginate.Cursor[models.Payment], error) PaymentInitiationRelatedPaymentsListAll(ctx context.Context, id models.PaymentInitiationID) ([]models.Payment, error) + // Payment Service Users + PaymentServiceUsersCreate(ctx context.Context, psu models.PaymentServiceUser) error + PaymentServiceUsersGet(ctx context.Context, id uuid.UUID) (*models.PaymentServiceUser, error) + PaymentServiceUsersList(ctx context.Context, query storage.ListPSUsQuery) (*bunpaginate.Cursor[models.PaymentServiceUser], error) + PaymentServiceUsersForwardBankAccountToConnector(ctx context.Context, psuID, bankAccountID uuid.UUID, connectorID models.ConnectorID) (models.Task, error) + PaymentServiceUsersAddBankAccount(ctx context.Context, psuID uuid.UUID, bankAccountID uuid.UUID) error + // Pools PoolsCreate(ctx context.Context, pool models.Pool) error PoolsGet(ctx context.Context, id uuid.UUID) (*models.Pool, error) diff --git a/internal/api/backend/backend_generated.go b/internal/api/backend/backend_generated.go index d3e04baee..82f70d661 100644 --- a/internal/api/backend/backend_generated.go +++ b/internal/api/backend/backend_generated.go @@ -489,6 +489,79 @@ func (mr *MockBackendMockRecorder) PaymentInitiationsRetry(ctx, id, waitResult a return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PaymentInitiationsRetry", reflect.TypeOf((*MockBackend)(nil).PaymentInitiationsRetry), ctx, id, waitResult) } +// PaymentServiceUsersAddBankAccount mocks base method. +func (m *MockBackend) PaymentServiceUsersAddBankAccount(ctx context.Context, psuID, bankAccountID uuid.UUID) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "PaymentServiceUsersAddBankAccount", ctx, psuID, bankAccountID) + ret0, _ := ret[0].(error) + return ret0 +} + +// PaymentServiceUsersAddBankAccount indicates an expected call of PaymentServiceUsersAddBankAccount. +func (mr *MockBackendMockRecorder) PaymentServiceUsersAddBankAccount(ctx, psuID, bankAccountID any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PaymentServiceUsersAddBankAccount", reflect.TypeOf((*MockBackend)(nil).PaymentServiceUsersAddBankAccount), ctx, psuID, bankAccountID) +} + +// PaymentServiceUsersCreate mocks base method. +func (m *MockBackend) PaymentServiceUsersCreate(ctx context.Context, psu models.PaymentServiceUser) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "PaymentServiceUsersCreate", ctx, psu) + ret0, _ := ret[0].(error) + return ret0 +} + +// PaymentServiceUsersCreate indicates an expected call of PaymentServiceUsersCreate. +func (mr *MockBackendMockRecorder) PaymentServiceUsersCreate(ctx, psu any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PaymentServiceUsersCreate", reflect.TypeOf((*MockBackend)(nil).PaymentServiceUsersCreate), ctx, psu) +} + +// PaymentServiceUsersForwardBankAccountToConnector mocks base method. +func (m *MockBackend) PaymentServiceUsersForwardBankAccountToConnector(ctx context.Context, psuID, bankAccountID uuid.UUID, connectorID models.ConnectorID) (models.Task, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "PaymentServiceUsersForwardBankAccountToConnector", ctx, psuID, bankAccountID, connectorID) + ret0, _ := ret[0].(models.Task) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// PaymentServiceUsersForwardBankAccountToConnector indicates an expected call of PaymentServiceUsersForwardBankAccountToConnector. +func (mr *MockBackendMockRecorder) PaymentServiceUsersForwardBankAccountToConnector(ctx, psuID, bankAccountID, connectorID any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PaymentServiceUsersForwardBankAccountToConnector", reflect.TypeOf((*MockBackend)(nil).PaymentServiceUsersForwardBankAccountToConnector), ctx, psuID, bankAccountID, connectorID) +} + +// PaymentServiceUsersGet mocks base method. +func (m *MockBackend) PaymentServiceUsersGet(ctx context.Context, id uuid.UUID) (*models.PaymentServiceUser, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "PaymentServiceUsersGet", ctx, id) + ret0, _ := ret[0].(*models.PaymentServiceUser) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// PaymentServiceUsersGet indicates an expected call of PaymentServiceUsersGet. +func (mr *MockBackendMockRecorder) PaymentServiceUsersGet(ctx, id any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PaymentServiceUsersGet", reflect.TypeOf((*MockBackend)(nil).PaymentServiceUsersGet), ctx, id) +} + +// PaymentServiceUsersList mocks base method. +func (m *MockBackend) PaymentServiceUsersList(ctx context.Context, query storage.ListPSUsQuery) (*bunpaginate.Cursor[models.PaymentServiceUser], error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "PaymentServiceUsersList", ctx, query) + ret0, _ := ret[0].(*bunpaginate.Cursor[models.PaymentServiceUser]) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// PaymentServiceUsersList indicates an expected call of PaymentServiceUsersList. +func (mr *MockBackendMockRecorder) PaymentServiceUsersList(ctx, query any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PaymentServiceUsersList", reflect.TypeOf((*MockBackend)(nil).PaymentServiceUsersList), ctx, query) +} + // PaymentsCreate mocks base method. func (m *MockBackend) PaymentsCreate(ctx context.Context, payment models.Payment) error { m.ctrl.T.Helper() diff --git a/internal/api/services/bank_accounts_forward_to_connector.go b/internal/api/services/bank_accounts_forward_to_connector.go index f469f232b..4e308e892 100644 --- a/internal/api/services/bank_accounts_forward_to_connector.go +++ b/internal/api/services/bank_accounts_forward_to_connector.go @@ -8,7 +8,17 @@ import ( ) func (s *Service) BankAccountsForwardToConnector(ctx context.Context, bankAccountID uuid.UUID, connectorID models.ConnectorID, waitResult bool) (models.Task, error) { - task, err := s.engine.ForwardBankAccount(ctx, bankAccountID, connectorID, waitResult) + ba, err := s.storage.BankAccountsGet(ctx, bankAccountID, true) + if err != nil { + return models.Task{}, newStorageError(err, "failed to get bank account") + } + + if ba == nil { + // Should not happened, but just in case + return models.Task{}, newStorageError(nil, "bank account not found") + } + + task, err := s.engine.ForwardBankAccount(ctx, *ba, connectorID, waitResult) if err != nil { return models.Task{}, handleEngineErrors(err) } diff --git a/internal/api/services/bank_accounts_forward_to_connector_test.go b/internal/api/services/bank_accounts_forward_to_connector_test.go index 00c07e562..4b1b5d234 100644 --- a/internal/api/services/bank_accounts_forward_to_connector_test.go +++ b/internal/api/services/bank_accounts_forward_to_connector_test.go @@ -29,46 +29,69 @@ func TestBankAccountsForwardToConnector(t *testing.T) { } tests := []struct { - name string - bankAccountID uuid.UUID - err error - expectedError error - typedError bool + name string + bankAccountID uuid.UUID + engineErr error + storageErr error + expectedEngineError error + expectedStorageError error + typedError bool }{ { - name: "success", - err: nil, - expectedError: nil, + name: "success", + engineErr: nil, }, { - name: "validation error", - err: engine.ErrValidation, - expectedError: ErrValidation, - typedError: true, + name: "validation error", + engineErr: engine.ErrValidation, + expectedEngineError: ErrValidation, + typedError: true, }, { - name: "not found error", - err: engine.ErrNotFound, - expectedError: ErrNotFound, - typedError: true, + name: "not found error", + engineErr: engine.ErrNotFound, + expectedEngineError: ErrNotFound, + typedError: true, }, { - name: "other error", - err: fmt.Errorf("error"), - expectedError: fmt.Errorf("error"), + name: "other error", + engineErr: fmt.Errorf("error"), + expectedEngineError: fmt.Errorf("error"), + }, + { + name: "storage error not found", + storageErr: storage.ErrNotFound, + typedError: true, + expectedStorageError: newStorageError(storage.ErrNotFound, "failed to get bank account"), + }, + { + name: "other error", + storageErr: fmt.Errorf("error"), + expectedStorageError: newStorageError(fmt.Errorf("error"), "failed to get bank account"), }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { - eng.EXPECT().ForwardBankAccount(gomock.Any(), test.bankAccountID, connectorID, false).Return(models.Task{}, test.err) + store.EXPECT().BankAccountsGet(gomock.Any(), test.bankAccountID, true).Return(&models.BankAccount{}, test.storageErr) + + if test.storageErr == nil { + eng.EXPECT().ForwardBankAccount(gomock.Any(), models.BankAccount{}, connectorID, false).Return(models.Task{}, test.engineErr) + } _, err := s.BankAccountsForwardToConnector(context.Background(), test.bankAccountID, connectorID, false) - if test.expectedError == nil { + switch { + case test.expectedEngineError != nil && test.typedError: + require.ErrorIs(t, err, test.expectedEngineError) + case test.expectedEngineError != nil && !test.typedError: + require.Error(t, err) + require.Equal(t, test.expectedEngineError.Error(), err.Error()) + case test.expectedStorageError != nil && test.typedError: + require.ErrorIs(t, err, test.expectedStorageError) + case test.expectedStorageError != nil && !test.typedError: + require.Error(t, err) + require.Equal(t, test.expectedStorageError.Error(), err.Error()) + default: require.NoError(t, err) - } else if test.typedError { - require.ErrorIs(t, err, test.expectedError) - } else { - require.Equal(t, test.expectedError, err) } }) } diff --git a/internal/api/services/payment_service_users_add_bank_account.go b/internal/api/services/payment_service_users_add_bank_account.go new file mode 100644 index 000000000..dbd1de6b3 --- /dev/null +++ b/internal/api/services/payment_service_users_add_bank_account.go @@ -0,0 +1,11 @@ +package services + +import ( + "context" + + "github.com/google/uuid" +) + +func (s *Service) PaymentServiceUsersAddBankAccount(ctx context.Context, psuID uuid.UUID, bankAccountID uuid.UUID) error { + return newStorageError(s.storage.PaymentServiceUsersAddBankAccount(ctx, psuID, bankAccountID), "failed to add bank account to payment service user") +} diff --git a/internal/api/services/payment_service_users_add_bank_account_test.go b/internal/api/services/payment_service_users_add_bank_account_test.go new file mode 100644 index 000000000..8d72e2db7 --- /dev/null +++ b/internal/api/services/payment_service_users_add_bank_account_test.go @@ -0,0 +1,59 @@ +package services + +import ( + "context" + "fmt" + "testing" + + "github.com/formancehq/payments/internal/connectors/engine" + "github.com/formancehq/payments/internal/storage" + "github.com/google/uuid" + "github.com/stretchr/testify/require" + gomock "go.uber.org/mock/gomock" +) + +func TestPaymentServiceUsersAddBankAccount(t *testing.T) { + t.Parallel() + + ctrl := gomock.NewController(t) + defer ctrl.Finish() + store := storage.NewMockStorage(ctrl) + eng := engine.NewMockEngine(ctrl) + + s := New(store, eng, false) + + tests := []struct { + name string + err error + expectedError error + }{ + { + name: "success", + err: nil, + expectedError: nil, + }, + { + name: "storage error not found", + err: storage.ErrNotFound, + expectedError: newStorageError(storage.ErrNotFound, "failed to add bank account to payment service user"), + }, + { + name: "other error", + err: fmt.Errorf("error"), + expectedError: newStorageError(fmt.Errorf("error"), "failed to add bank account to payment service user"), + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + psuID, baID := uuid.New(), uuid.New() + store.EXPECT().PaymentServiceUsersAddBankAccount(gomock.Any(), psuID, baID).Return(test.err) + err := s.PaymentServiceUsersAddBankAccount(context.Background(), psuID, baID) + if test.expectedError == nil { + require.NoError(t, err) + } else { + require.Equal(t, test.expectedError, err) + } + }) + } +} diff --git a/internal/api/services/payment_service_users_create.go b/internal/api/services/payment_service_users_create.go new file mode 100644 index 000000000..d5d83115c --- /dev/null +++ b/internal/api/services/payment_service_users_create.go @@ -0,0 +1,11 @@ +package services + +import ( + "context" + + "github.com/formancehq/payments/internal/models" +) + +func (s *Service) PaymentServiceUsersCreate(ctx context.Context, psu models.PaymentServiceUser) error { + return newStorageError(s.storage.PaymentServiceUsersCreate(ctx, psu), "cannot create payment service user") +} diff --git a/internal/api/services/payment_service_users_create_test.go b/internal/api/services/payment_service_users_create_test.go new file mode 100644 index 000000000..09ea406c1 --- /dev/null +++ b/internal/api/services/payment_service_users_create_test.go @@ -0,0 +1,58 @@ +package services + +import ( + "context" + "fmt" + "testing" + + "github.com/formancehq/payments/internal/connectors/engine" + "github.com/formancehq/payments/internal/models" + "github.com/formancehq/payments/internal/storage" + "github.com/stretchr/testify/require" + gomock "go.uber.org/mock/gomock" +) + +func TestPSUCreate(t *testing.T) { + t.Parallel() + + ctrl := gomock.NewController(t) + defer ctrl.Finish() + store := storage.NewMockStorage(ctrl) + eng := engine.NewMockEngine(ctrl) + + s := New(store, eng, false) + + tests := []struct { + name string + err error + expectedError error + }{ + { + name: "success", + err: nil, + expectedError: nil, + }, + { + name: "storage error not found", + err: storage.ErrNotFound, + expectedError: newStorageError(storage.ErrNotFound, "cannot create payment service user"), + }, + { + name: "other error", + err: fmt.Errorf("error"), + expectedError: newStorageError(fmt.Errorf("error"), "cannot create payment service user"), + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + store.EXPECT().PaymentServiceUsersCreate(gomock.Any(), models.PaymentServiceUser{}).Return(test.err) + err := s.PaymentServiceUsersCreate(context.Background(), models.PaymentServiceUser{}) + if test.expectedError == nil { + require.NoError(t, err) + } else { + require.Equal(t, test.expectedError, err) + } + }) + } +} diff --git a/internal/api/services/payment_service_users_forward_bank_account.go b/internal/api/services/payment_service_users_forward_bank_account.go new file mode 100644 index 000000000..c5e2aa1c0 --- /dev/null +++ b/internal/api/services/payment_service_users_forward_bank_account.go @@ -0,0 +1,35 @@ +package services + +import ( + "context" + + "github.com/formancehq/payments/internal/models" + "github.com/formancehq/payments/internal/storage" + "github.com/google/uuid" +) + +func (s *Service) PaymentServiceUsersForwardBankAccountToConnector(ctx context.Context, psuID, bankAccountID uuid.UUID, connectorID models.ConnectorID) (models.Task, error) { + ba, err := s.storage.BankAccountsGet(ctx, bankAccountID, true) + if err != nil { + return models.Task{}, newStorageError(err, "failed to get bank account") + } + + if ba == nil { + // Should not happened, but just in case + return models.Task{}, newStorageError(storage.ErrNotFound, "bank account not found") + } + + psu, err := s.storage.PaymentServiceUsersGet(ctx, psuID) + if err != nil { + return models.Task{}, newStorageError(err, "failed to get payment service user") + } + + models.FillBankAccountMetadataWithPaymentServiceUserInfo(ba, psu) + + task, err := s.engine.ForwardBankAccount(ctx, *ba, connectorID, false) + if err != nil { + return models.Task{}, handleEngineErrors(err) + } + + return task, nil +} diff --git a/internal/api/services/payment_service_users_forward_bank_account_test.go b/internal/api/services/payment_service_users_forward_bank_account_test.go new file mode 100644 index 000000000..8d0f2774c --- /dev/null +++ b/internal/api/services/payment_service_users_forward_bank_account_test.go @@ -0,0 +1,123 @@ +package services + +import ( + "context" + "fmt" + "testing" + + "github.com/formancehq/payments/internal/connectors/engine" + "github.com/formancehq/payments/internal/models" + "github.com/formancehq/payments/internal/storage" + "github.com/google/uuid" + "github.com/stretchr/testify/require" + gomock "go.uber.org/mock/gomock" +) + +func TestPaymentServiceUsersForwardBankAccountsToConnector(t *testing.T) { + t.Parallel() + + ctrl := gomock.NewController(t) + defer ctrl.Finish() + store := storage.NewMockStorage(ctrl) + eng := engine.NewMockEngine(ctrl) + + s := New(store, eng, false) + + connectorID := models.ConnectorID{ + Reference: uuid.New(), + Provider: "test", + } + + tests := []struct { + name string + bankAccountID uuid.UUID + psuID uuid.UUID + engineErr error + bankAccountStorageErr error + psuStorageErr error + expectedEngineError error + expectedStorageError error + typedError bool + }{ + { + name: "success", + bankAccountID: uuid.New(), + psuID: uuid.New(), + engineErr: nil, + }, + { + name: "validation error", + bankAccountID: uuid.New(), + psuID: uuid.New(), + engineErr: engine.ErrValidation, + expectedEngineError: ErrValidation, + typedError: true, + }, + { + name: "not found error", + bankAccountID: uuid.New(), + psuID: uuid.New(), + engineErr: engine.ErrNotFound, + expectedEngineError: ErrNotFound, + typedError: true, + }, + { + name: "other error", + bankAccountID: uuid.New(), + psuID: uuid.New(), + engineErr: fmt.Errorf("error"), + expectedEngineError: fmt.Errorf("error"), + }, + { + name: "bank account storage error not found", + bankAccountStorageErr: storage.ErrNotFound, + typedError: true, + expectedStorageError: newStorageError(storage.ErrNotFound, "failed to get bank account"), + }, + { + name: "bank account other error", + bankAccountStorageErr: fmt.Errorf("error"), + expectedStorageError: newStorageError(fmt.Errorf("error"), "failed to get bank account"), + }, + { + name: "psu storage error not found", + psuStorageErr: storage.ErrNotFound, + typedError: true, + expectedStorageError: newStorageError(storage.ErrNotFound, "failed to get payment service user"), + }, + { + name: "psu other error", + psuStorageErr: fmt.Errorf("error"), + expectedStorageError: newStorageError(fmt.Errorf("error"), "failed to get payment service user"), + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + store.EXPECT().BankAccountsGet(gomock.Any(), test.bankAccountID, true).Return(&models.BankAccount{}, test.bankAccountStorageErr) + + if test.bankAccountStorageErr == nil { + store.EXPECT().PaymentServiceUsersGet(gomock.Any(), test.psuID).Return(&models.PaymentServiceUser{}, test.psuStorageErr) + + if test.psuStorageErr == nil { + eng.EXPECT().ForwardBankAccount(gomock.Any(), models.BankAccount{}, connectorID, false).Return(models.Task{}, test.engineErr) + } + } + _, err := s.PaymentServiceUsersForwardBankAccountToConnector(context.Background(), test.psuID, test.bankAccountID, connectorID) + switch { + case test.expectedEngineError != nil && test.typedError: + require.ErrorIs(t, err, test.expectedEngineError) + case test.expectedEngineError != nil && !test.typedError: + require.Error(t, err) + require.Equal(t, test.expectedEngineError.Error(), err.Error()) + case test.expectedStorageError != nil && test.typedError: + require.ErrorIs(t, err, test.expectedStorageError) + case test.expectedStorageError != nil && !test.typedError: + require.Error(t, err) + require.Equal(t, test.expectedStorageError.Error(), err.Error()) + default: + require.NoError(t, err) + } + }) + } +} diff --git a/internal/api/services/payment_service_users_get.go b/internal/api/services/payment_service_users_get.go new file mode 100644 index 000000000..d667a7f38 --- /dev/null +++ b/internal/api/services/payment_service_users_get.go @@ -0,0 +1,17 @@ +package services + +import ( + "context" + + "github.com/formancehq/payments/internal/models" + "github.com/google/uuid" +) + +func (s *Service) PaymentServiceUsersGet(ctx context.Context, id uuid.UUID) (*models.PaymentServiceUser, error) { + psu, err := s.storage.PaymentServiceUsersGet(ctx, id) + if err != nil { + return nil, newStorageError(err, "cannot get payment service user") + } + + return psu, nil +} diff --git a/internal/api/services/payment_service_users_get_test.go b/internal/api/services/payment_service_users_get_test.go new file mode 100644 index 000000000..5f09ac5dd --- /dev/null +++ b/internal/api/services/payment_service_users_get_test.go @@ -0,0 +1,62 @@ +package services + +import ( + "context" + "fmt" + "testing" + + "github.com/formancehq/payments/internal/connectors/engine" + "github.com/formancehq/payments/internal/models" + "github.com/formancehq/payments/internal/storage" + "github.com/google/uuid" + "github.com/stretchr/testify/require" + gomock "go.uber.org/mock/gomock" +) + +func TestPSUGet(t *testing.T) { + t.Parallel() + + ctrl := gomock.NewController(t) + defer ctrl.Finish() + store := storage.NewMockStorage(ctrl) + eng := engine.NewMockEngine(ctrl) + + s := New(store, eng, false) + + id := uuid.New() + + tests := []struct { + name string + err error + expectedError error + }{ + { + name: "success", + err: nil, + expectedError: nil, + }, + { + name: "storage error not found", + err: storage.ErrNotFound, + expectedError: newStorageError(storage.ErrNotFound, "cannot get payment service user"), + }, + { + name: "other error", + err: fmt.Errorf("error"), + expectedError: newStorageError(fmt.Errorf("error"), "cannot get payment service user"), + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + store.EXPECT().PaymentServiceUsersGet(gomock.Any(), id).Return(&models.PaymentServiceUser{}, test.err) + bankAccount, err := s.PaymentServiceUsersGet(context.Background(), id) + if test.expectedError == nil { + require.NotNil(t, bankAccount) + require.NoError(t, err) + } else { + require.Equal(t, test.expectedError, err) + } + }) + } +} diff --git a/internal/api/services/payment_service_users_list.go b/internal/api/services/payment_service_users_list.go new file mode 100644 index 000000000..eeb1624ad --- /dev/null +++ b/internal/api/services/payment_service_users_list.go @@ -0,0 +1,18 @@ +package services + +import ( + "context" + + "github.com/formancehq/go-libs/v3/bun/bunpaginate" + "github.com/formancehq/payments/internal/models" + "github.com/formancehq/payments/internal/storage" +) + +func (s *Service) PaymentServiceUsersList(ctx context.Context, query storage.ListPSUsQuery) (*bunpaginate.Cursor[models.PaymentServiceUser], error) { + psus, err := s.storage.PaymentServiceUsersList(ctx, query) + if err != nil { + return nil, newStorageError(err, "cannot list payment service users") + } + + return psus, nil +} diff --git a/internal/api/services/payment_service_users_list_test.go b/internal/api/services/payment_service_users_list_test.go new file mode 100644 index 000000000..e3ad92993 --- /dev/null +++ b/internal/api/services/payment_service_users_list_test.go @@ -0,0 +1,58 @@ +package services + +import ( + "context" + "fmt" + "testing" + + "github.com/formancehq/payments/internal/connectors/engine" + "github.com/formancehq/payments/internal/storage" + "github.com/stretchr/testify/require" + gomock "go.uber.org/mock/gomock" +) + +func TestPSUList(t *testing.T) { + t.Parallel() + + ctrl := gomock.NewController(t) + defer ctrl.Finish() + store := storage.NewMockStorage(ctrl) + eng := engine.NewMockEngine(ctrl) + + s := New(store, eng, false) + + tests := []struct { + name string + err error + expectedError error + }{ + { + name: "success", + err: nil, + expectedError: nil, + }, + { + name: "storage error not found", + err: storage.ErrNotFound, + expectedError: newStorageError(storage.ErrNotFound, "cannot list payment service users"), + }, + { + name: "other error", + err: fmt.Errorf("error"), + expectedError: newStorageError(fmt.Errorf("error"), "cannot list payment service users"), + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + query := storage.ListPSUsQuery{} + store.EXPECT().PaymentServiceUsersList(gomock.Any(), query).Return(nil, test.err) + _, err := s.PaymentServiceUsersList(context.Background(), query) + if test.expectedError == nil { + require.NoError(t, err) + } else { + require.Equal(t, test.expectedError, err) + } + }) + } +} diff --git a/internal/api/v3/errors.go b/internal/api/v3/errors.go index 83d5f74b8..c7b3e8eb9 100644 --- a/internal/api/v3/errors.go +++ b/internal/api/v3/errors.go @@ -1,13 +1,13 @@ package v3 import ( - "errors" "net/http" "github.com/formancehq/go-libs/v3/api" "github.com/formancehq/payments/internal/api/common" "github.com/formancehq/payments/internal/api/services" "github.com/formancehq/payments/internal/storage" + "github.com/pkg/errors" ) const ( @@ -23,6 +23,8 @@ func handleServiceErrors(w http.ResponseWriter, r *http.Request, err error) { api.BadRequest(w, ErrUniqueReference, err) case errors.Is(err, storage.ErrNotFound): api.NotFound(w, err) + case errors.Is(err, storage.ErrForeignKeyViolation): + api.BadRequest(w, ErrValidation, errors.Cause(err)) case errors.Is(err, storage.ErrValidation): api.BadRequest(w, ErrValidation, err) case errors.Is(err, services.ErrValidation): diff --git a/internal/api/v3/handler_payment_service_users_add_bank_account.go b/internal/api/v3/handler_payment_service_users_add_bank_account.go new file mode 100644 index 000000000..606d7a645 --- /dev/null +++ b/internal/api/v3/handler_payment_service_users_add_bank_account.go @@ -0,0 +1,43 @@ +package v3 + +import ( + "net/http" + + "github.com/formancehq/go-libs/v3/api" + "github.com/formancehq/payments/internal/api/backend" + "github.com/formancehq/payments/internal/otel" + "github.com/google/uuid" + "go.opentelemetry.io/otel/attribute" +) + +func paymentServiceUsersAddBankAccount(backend backend.Backend) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + ctx, span := otel.Tracer().Start(r.Context(), "v3_paymentServiceUsersAddBankAccount") + defer span.End() + + span.SetAttributes(attribute.String("paymentServiceUserID", paymentServiceUserID(r))) + id, err := uuid.Parse(paymentServiceUserID(r)) + if err != nil { + otel.RecordError(span, err) + api.BadRequest(w, ErrInvalidID, err) + return + } + + span.SetAttributes(attribute.String("bankAccountID", bankAccountID(r))) + bankAccountID, err := uuid.Parse(bankAccountID(r)) + if err != nil { + otel.RecordError(span, err) + api.BadRequest(w, ErrInvalidID, err) + return + } + + err = backend.PaymentServiceUsersAddBankAccount(ctx, id, bankAccountID) + if err != nil { + otel.RecordError(span, err) + handleServiceErrors(w, r, err) + return + } + + api.NoContent(w) + } +} diff --git a/internal/api/v3/handler_payment_service_users_add_bank_account_test.go b/internal/api/v3/handler_payment_service_users_add_bank_account_test.go new file mode 100644 index 000000000..ef0639552 --- /dev/null +++ b/internal/api/v3/handler_payment_service_users_add_bank_account_test.go @@ -0,0 +1,64 @@ +package v3 + +import ( + "errors" + "net/http" + "net/http/httptest" + + "github.com/formancehq/payments/internal/api/backend" + "github.com/google/uuid" + . "github.com/onsi/ginkgo/v2" + "go.uber.org/mock/gomock" +) + +var _ = Describe("API v3 Payment Service Users Add Bank Account", func() { + var ( + handlerFn http.HandlerFunc + bankAccountID uuid.UUID + psuID uuid.UUID + ) + BeforeEach(func() { + bankAccountID = uuid.New() + psuID = uuid.New() + }) + + Context("PSU add bank account", func() { + var ( + w *httptest.ResponseRecorder + m *backend.MockBackend + ) + BeforeEach(func() { + w = httptest.NewRecorder() + ctrl := gomock.NewController(GinkgoT()) + m = backend.NewMockBackend(ctrl) + handlerFn = paymentServiceUsersAddBankAccount(m) + }) + + It("should return a bad request error when psu id is invalid", func(ctx SpecContext) { + req := prepareQueryRequest(http.MethodGet, "paymentServiceUserID", "invalid") + handlerFn(w, req) + + assertExpectedResponse(w.Result(), http.StatusBadRequest, ErrInvalidID) + }) + + It("should return a bad request error when bank account id is invalid", func(ctx SpecContext) { + req := prepareQueryRequest(http.MethodGet, "paymentServiceUserID", psuID.String(), "bankAccountID", "invalid") + handlerFn(w, req) + + assertExpectedResponse(w.Result(), http.StatusBadRequest, ErrInvalidID) + }) + + It("should return an internal server error when backend returns error", func(ctx SpecContext) { + expectedErr := errors.New("psu add account err") + m.EXPECT().PaymentServiceUsersAddBankAccount(gomock.Any(), gomock.Any(), gomock.Any()).Return(expectedErr) + handlerFn(w, prepareQueryRequest(http.MethodGet, "paymentServiceUserID", psuID.String(), "bankAccountID", bankAccountID.String())) + assertExpectedResponse(w.Result(), http.StatusInternalServerError, "INTERNAL") + }) + + It("should return status no content on success", func(ctx SpecContext) { + m.EXPECT().PaymentServiceUsersAddBankAccount(gomock.Any(), psuID, bankAccountID).Return(nil) + handlerFn(w, prepareQueryRequest(http.MethodGet, "paymentServiceUserID", psuID.String(), "bankAccountID", bankAccountID.String())) + assertExpectedResponse(w.Result(), http.StatusNoContent, "") + }) + }) +}) diff --git a/internal/api/v3/handler_payment_service_users_create.go b/internal/api/v3/handler_payment_service_users_create.go new file mode 100644 index 000000000..bd9199c4b --- /dev/null +++ b/internal/api/v3/handler_payment_service_users_create.go @@ -0,0 +1,124 @@ +package v3 + +import ( + "encoding/json" + "fmt" + "net/http" + "time" + + "github.com/formancehq/go-libs/v3/api" + "github.com/formancehq/payments/internal/api/backend" + "github.com/formancehq/payments/internal/api/validation" + "github.com/formancehq/payments/internal/models" + "github.com/formancehq/payments/internal/otel" + "github.com/google/uuid" + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/trace" +) + +type ContactDetailsRequest struct { + Email *string `json:"email,omitempty"` + PhoneNumber *string `json:"phoneNumber,omitempty"` +} + +type AddressRequest struct { + StreetName *string `json:"streetName,omitempty"` + StreetNumber *string `json:"streetNumber,omitempty" validate:"omitempty,number"` + City *string `json:"city,omitempty"` + Region *string `json:"region,omitempty"` + PostalCode *string `json:"postalCode,omitempty"` + Country *string `json:"country,omitempty" validate:"omitempty,country_code"` +} + +type PaymentServiceUsersCreateRequest struct { + Name string `json:"name" validate:"required,lte=1000"` + + ContactDetails *ContactDetailsRequest `json:"contactDetails,omitempty"` + Address *AddressRequest `json:"address,omitempty"` + BankAccountIDs []string `json:"bankAccountIDs,omitempty" validate:"omitempty,dive,uuid"` + Metadata map[string]string `json:"metadata,omitempty" validate:""` +} + +func paymentServiceUsersCreate(backend backend.Backend, validator *validation.Validator) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + ctx, span := otel.Tracer().Start(r.Context(), "v3_paymentServiceUsersCreate") + defer span.End() + + var req PaymentServiceUsersCreateRequest + err := json.NewDecoder(r.Body).Decode(&req) + if err != nil { + otel.RecordError(span, err) + api.BadRequest(w, ErrMissingOrInvalidBody, err) + return + } + + populateSpanFromPaymentServiceUserCreateRequest(span, req) + + if _, err := validator.Validate(req); err != nil { + otel.RecordError(span, err) + api.BadRequest(w, ErrValidation, err) + return + } + + bankAccountIDs := make([]uuid.UUID, len(req.BankAccountIDs)) + for i, id := range req.BankAccountIDs { + bankAccountIDs[i], err = uuid.Parse(id) + if err != nil { + otel.RecordError(span, err) + api.BadRequest(w, ErrValidation, err) + return + } + } + + paymentServiceUser := models.PaymentServiceUser{ + ID: uuid.New(), + Name: req.Name, + CreatedAt: time.Now().UTC(), + ContactDetails: func() *models.ContactDetails { + if req.ContactDetails == nil { + return nil + } + + return &models.ContactDetails{ + Email: req.ContactDetails.Email, + PhoneNumber: req.ContactDetails.PhoneNumber, + } + }(), + Address: func() *models.Address { + if req.Address == nil { + return nil + } + + return &models.Address{ + StreetName: req.Address.StreetName, + StreetNumber: req.Address.StreetNumber, + City: req.Address.City, + Region: req.Address.Region, + PostalCode: req.Address.PostalCode, + Country: req.Address.Country, + } + }(), + BankAccountIDs: bankAccountIDs, + Metadata: req.Metadata, + } + + err = backend.PaymentServiceUsersCreate(ctx, paymentServiceUser) + if err != nil { + otel.RecordError(span, err) + handleServiceErrors(w, r, err) + return + } + + api.Created(w, paymentServiceUser.ID.String()) + } +} + +func populateSpanFromPaymentServiceUserCreateRequest(span trace.Span, req PaymentServiceUsersCreateRequest) { + span.SetAttributes(attribute.String("name", req.Name)) + + // Do not record other information as they are sensitive information + + for k, v := range req.Metadata { + span.SetAttributes(attribute.String(fmt.Sprintf("metadata[%s]", k), v)) + } +} diff --git a/internal/api/v3/handler_payment_service_users_create_test.go b/internal/api/v3/handler_payment_service_users_create_test.go new file mode 100644 index 000000000..3caf6ae43 --- /dev/null +++ b/internal/api/v3/handler_payment_service_users_create_test.go @@ -0,0 +1,101 @@ +package v3 + +import ( + "errors" + "net/http" + "net/http/httptest" + + "github.com/formancehq/go-libs/v3/pointer" + "github.com/formancehq/payments/internal/api/backend" + "github.com/formancehq/payments/internal/api/validation" + "github.com/google/uuid" + . "github.com/onsi/ginkgo/v2" + "go.uber.org/mock/gomock" +) + +var _ = Describe("API v3 Payment Service Users Create", func() { + var ( + handlerFn http.HandlerFunc + bankAccountIDs []string + ) + BeforeEach(func() { + bankAccountIDs = []string{uuid.New().String(), uuid.New().String()} + }) + + Context("create psu", func() { + var ( + w *httptest.ResponseRecorder + m *backend.MockBackend + ) + BeforeEach(func() { + w = httptest.NewRecorder() + ctrl := gomock.NewController(GinkgoT()) + m = backend.NewMockBackend(ctrl) + handlerFn = paymentServiceUsersCreate(m, validation.NewValidator()) + }) + + It("should return a bad request error when body is missing", func(ctx SpecContext) { + req := httptest.NewRequest(http.MethodPost, "/", nil) + handlerFn(w, req) + + assertExpectedResponse(w.Result(), http.StatusBadRequest, ErrMissingOrInvalidBody) + }) + + DescribeTable("validation errors", + func(psuReq PaymentServiceUsersCreateRequest) { + handlerFn(w, prepareJSONRequest(http.MethodPost, &psuReq)) + assertExpectedResponse(w.Result(), http.StatusBadRequest, ErrValidation) + }, + Entry("name missing", PaymentServiceUsersCreateRequest{}), + Entry("name too long", PaymentServiceUsersCreateRequest{Name: generateTextString(1001)}), + Entry("country invalid", PaymentServiceUsersCreateRequest{Name: "a", Address: &AddressRequest{Country: pointer.For("invalid")}}), + Entry("street number invalid", PaymentServiceUsersCreateRequest{Name: "a", Address: &AddressRequest{StreetNumber: pointer.For("invalid")}}), + ) + + It("should return an internal server error when backend returns error", func(ctx SpecContext) { + expectedErr := errors.New("psu create err") + m.EXPECT().PaymentServiceUsersCreate(gomock.Any(), gomock.Any()).Return(expectedErr) + psuReq := PaymentServiceUsersCreateRequest{ + Name: "reference", + BankAccountIDs: bankAccountIDs, + } + handlerFn(w, prepareJSONRequest(http.MethodPost, &psuReq)) + assertExpectedResponse(w.Result(), http.StatusInternalServerError, "INTERNAL") + }) + + It("should return status created", func(ctx SpecContext) { + m.EXPECT().PaymentServiceUsersCreate(gomock.Any(), gomock.Any()).Return(nil) + psuReq := PaymentServiceUsersCreateRequest{ + Name: "reference", + BankAccountIDs: bankAccountIDs, + } + handlerFn(w, prepareJSONRequest(http.MethodPost, &psuReq)) + assertExpectedResponse(w.Result(), http.StatusCreated, "data") + }) + + It("should return status created with optional fields", func(ctx SpecContext) { + m.EXPECT().PaymentServiceUsersCreate(gomock.Any(), gomock.Any()).Return(nil) + psuReq := PaymentServiceUsersCreateRequest{ + Name: "reference", + ContactDetails: &ContactDetailsRequest{ + Email: pointer.For("test"), + PhoneNumber: pointer.For("test"), + }, + Address: &AddressRequest{ + StreetName: pointer.For("test"), + StreetNumber: pointer.For("1"), + City: pointer.For("test"), + Region: pointer.For("test"), + PostalCode: pointer.For("test"), + Country: pointer.For("FR"), + }, + BankAccountIDs: bankAccountIDs, + Metadata: map[string]string{ + "foo": "bar", + }, + } + handlerFn(w, prepareJSONRequest(http.MethodPost, &psuReq)) + assertExpectedResponse(w.Result(), http.StatusCreated, "data") + }) + }) +}) diff --git a/internal/api/v3/handler_payment_service_users_forward_bank_account_to_connector.go b/internal/api/v3/handler_payment_service_users_forward_bank_account_to_connector.go new file mode 100644 index 000000000..796f636f2 --- /dev/null +++ b/internal/api/v3/handler_payment_service_users_forward_bank_account_to_connector.go @@ -0,0 +1,75 @@ +package v3 + +import ( + "encoding/json" + "net/http" + + "github.com/formancehq/go-libs/v3/api" + "github.com/formancehq/payments/internal/api/backend" + "github.com/formancehq/payments/internal/api/validation" + "github.com/formancehq/payments/internal/models" + "github.com/formancehq/payments/internal/otel" + "github.com/google/uuid" + "go.opentelemetry.io/otel/attribute" +) + +type PaymentServiceUserForwardBankAccountToConnectorRequest struct { + ConnectorID string `json:"connectorID" validate:"required,connectorID"` +} + +type PaymentServiceUserForwardBankAccountToConnectorResponse struct { + TaskID string `json:"taskID"` +} + +func paymentServiceUsersForwardBankAccountToConnector(backend backend.Backend, validator *validation.Validator) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + ctx, span := otel.Tracer().Start(r.Context(), "v3_paymentServiceUsersForwardBankAccountToConnector") + defer span.End() + + span.SetAttributes(attribute.String("paymentServiceUserID", paymentServiceUserID(r))) + id, err := uuid.Parse(paymentServiceUserID(r)) + if err != nil { + otel.RecordError(span, err) + api.BadRequest(w, ErrInvalidID, err) + return + } + + span.SetAttributes(attribute.String("bankAccountID", bankAccountID(r))) + bankAccountID, err := uuid.Parse(bankAccountID(r)) + if err != nil { + otel.RecordError(span, err) + api.BadRequest(w, ErrInvalidID, err) + return + } + + var req PaymentServiceUserForwardBankAccountToConnectorRequest + err = json.NewDecoder(r.Body).Decode(&req) + if err != nil { + otel.RecordError(span, err) + api.BadRequest(w, ErrMissingOrInvalidBody, err) + return + } + + span.SetAttributes(attribute.String("connectorID", req.ConnectorID)) + + _, err = validator.Validate(req) + if err != nil { + otel.RecordError(span, err) + api.BadRequest(w, ErrValidation, err) + return + } + + connectorID := models.MustConnectorIDFromString(req.ConnectorID) + + task, err := backend.PaymentServiceUsersForwardBankAccountToConnector(ctx, id, bankAccountID, connectorID) + if err != nil { + otel.RecordError(span, err) + handleServiceErrors(w, r, err) + return + } + + api.Accepted(w, PaymentServiceUserForwardBankAccountToConnectorResponse{ + TaskID: task.ID.String(), + }) + } +} diff --git a/internal/api/v3/handler_payment_service_users_forward_bank_account_to_connector_test.go b/internal/api/v3/handler_payment_service_users_forward_bank_account_to_connector_test.go new file mode 100644 index 000000000..9d4a18259 --- /dev/null +++ b/internal/api/v3/handler_payment_service_users_forward_bank_account_to_connector_test.go @@ -0,0 +1,85 @@ +package v3 + +import ( + "bytes" + "encoding/json" + "fmt" + "net/http" + "net/http/httptest" + + "github.com/formancehq/payments/internal/api/backend" + "github.com/formancehq/payments/internal/api/validation" + "github.com/formancehq/payments/internal/models" + "github.com/google/uuid" + . "github.com/onsi/ginkgo/v2" + "go.uber.org/mock/gomock" +) + +var _ = Describe("API v3 Payment Service Users Bank Accounts ForwardToConnector", func() { + var ( + handlerFn http.HandlerFunc + bankAccountID uuid.UUID + psuID uuid.UUID + connID models.ConnectorID + ) + BeforeEach(func() { + psuID = uuid.New() + bankAccountID = uuid.New() + connID = models.ConnectorID{Reference: uuid.New(), Provider: "psp"} + }) + + Context("forward psu bank accounts to connector", func() { + var ( + w *httptest.ResponseRecorder + m *backend.MockBackend + freq PaymentServiceUserForwardBankAccountToConnectorRequest + ) + BeforeEach(func() { + w = httptest.NewRecorder() + ctrl := gomock.NewController(GinkgoT()) + m = backend.NewMockBackend(ctrl) + handlerFn = paymentServiceUsersForwardBankAccountToConnector(m, validation.NewValidator()) + }) + + DescribeTable("validation errors", + func(expected string, freq PaymentServiceUserForwardBankAccountToConnectorRequest, psuID string, baID string) { + b, _ := json.Marshal(freq) + body := bytes.NewReader(b) + handlerFn(w, prepareQueryRequestWithBody(http.MethodPost, body, "paymentServiceUserID", psuID, "bankAccountID", baID)) + assertExpectedResponse(w.Result(), http.StatusBadRequest, expected) + }, + Entry("psu ID invalid", ErrInvalidID, PaymentServiceUserForwardBankAccountToConnectorRequest{}, "invalid", bankAccountID.String()), + Entry("bank account ID ID invalid", ErrInvalidID, PaymentServiceUserForwardBankAccountToConnectorRequest{}, psuID.String(), "invalid"), + Entry("connector ID missing", ErrValidation, PaymentServiceUserForwardBankAccountToConnectorRequest{}, psuID.String(), bankAccountID.String()), + Entry("connector ID invalid", ErrValidation, PaymentServiceUserForwardBankAccountToConnectorRequest{ConnectorID: "blah"}, psuID.String(), bankAccountID.String()), + ) + + It("should return an internal server error when backend returns error", func(ctx SpecContext) { + m.EXPECT().PaymentServiceUsersForwardBankAccountToConnector(gomock.Any(), psuID, bankAccountID, connID).Return( + models.Task{}, + fmt.Errorf("bank account forward err"), + ) + freq = PaymentServiceUserForwardBankAccountToConnectorRequest{ + ConnectorID: connID.String(), + } + b, _ := json.Marshal(freq) + body := bytes.NewReader(b) + handlerFn(w, prepareQueryRequestWithBody(http.MethodPost, body, "paymentServiceUserID", psuID.String(), "bankAccountID", bankAccountID.String())) + assertExpectedResponse(w.Result(), http.StatusInternalServerError, "INTERNAL") + }) + + It("should return status accepted on success", func(ctx SpecContext) { + m.EXPECT().PaymentServiceUsersForwardBankAccountToConnector(gomock.Any(), psuID, bankAccountID, connID).Return( + models.Task{}, + nil, + ) + freq = PaymentServiceUserForwardBankAccountToConnectorRequest{ + ConnectorID: connID.String(), + } + b, _ := json.Marshal(freq) + body := bytes.NewReader(b) + handlerFn(w, prepareQueryRequestWithBody(http.MethodPost, body, "paymentServiceUserID", psuID.String(), "bankAccountID", bankAccountID.String())) + assertExpectedResponse(w.Result(), http.StatusAccepted, "data") + }) + }) +}) diff --git a/internal/api/v3/handler_payment_service_users_get.go b/internal/api/v3/handler_payment_service_users_get.go new file mode 100644 index 000000000..23da4aeea --- /dev/null +++ b/internal/api/v3/handler_payment_service_users_get.go @@ -0,0 +1,35 @@ +package v3 + +import ( + "net/http" + + "github.com/formancehq/go-libs/v3/api" + "github.com/formancehq/payments/internal/api/backend" + "github.com/formancehq/payments/internal/otel" + "github.com/google/uuid" + "go.opentelemetry.io/otel/attribute" +) + +func paymentServiceUsersGet(backend backend.Backend) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + ctx, span := otel.Tracer().Start(r.Context(), "v3_paymentServiceUsersGet") + defer span.End() + + span.SetAttributes(attribute.String("paymentServiceUserID", paymentServiceUserID(r))) + id, err := uuid.Parse(paymentServiceUserID(r)) + if err != nil { + otel.RecordError(span, err) + api.BadRequest(w, ErrInvalidID, err) + return + } + + paymentServiceUser, err := backend.PaymentServiceUsersGet(ctx, id) + if err != nil { + otel.RecordError(span, err) + handleServiceErrors(w, r, err) + return + } + + api.Ok(w, paymentServiceUser) + } +} diff --git a/internal/api/v3/handler_payment_service_users_get_test.go b/internal/api/v3/handler_payment_service_users_get_test.go new file mode 100644 index 000000000..7ff0789ef --- /dev/null +++ b/internal/api/v3/handler_payment_service_users_get_test.go @@ -0,0 +1,63 @@ +package v3 + +import ( + "fmt" + "net/http" + "net/http/httptest" + + "github.com/formancehq/payments/internal/api/backend" + "github.com/formancehq/payments/internal/models" + "github.com/google/uuid" + . "github.com/onsi/ginkgo/v2" + "go.uber.org/mock/gomock" +) + +var _ = Describe("API v3 Payment Service Users Get", func() { + var ( + handlerFn http.HandlerFunc + psuID uuid.UUID + ) + BeforeEach(func() { + psuID = uuid.New() + }) + + Context("get psu", func() { + var ( + w *httptest.ResponseRecorder + m *backend.MockBackend + ) + BeforeEach(func() { + w = httptest.NewRecorder() + ctrl := gomock.NewController(GinkgoT()) + m = backend.NewMockBackend(ctrl) + handlerFn = paymentServiceUsersGet(m) + }) + + It("should return an invalid ID error when psu ID is invalid", func(ctx SpecContext) { + req := prepareQueryRequest(http.MethodGet, "paymentServiceUserID", "invalidvalue") + handlerFn(w, req) + + assertExpectedResponse(w.Result(), http.StatusBadRequest, ErrInvalidID) + }) + + It("should return an internal server error when backend returns error", func(ctx SpecContext) { + req := prepareQueryRequest(http.MethodGet, "paymentServiceUserID", psuID.String()) + m.EXPECT().PaymentServiceUsersGet(gomock.Any(), psuID).Return( + &models.PaymentServiceUser{}, fmt.Errorf("psu get get error"), + ) + handlerFn(w, req) + + assertExpectedResponse(w.Result(), http.StatusInternalServerError, "INTERNAL") + }) + + It("should return data object", func(ctx SpecContext) { + req := prepareQueryRequest(http.MethodGet, "paymentServiceUserID", psuID.String()) + m.EXPECT().PaymentServiceUsersGet(gomock.Any(), psuID).Return( + &models.PaymentServiceUser{}, nil, + ) + handlerFn(w, req) + + assertExpectedResponse(w.Result(), http.StatusOK, "data") + }) + }) +}) diff --git a/internal/api/v3/handler_payment_service_users_list.go b/internal/api/v3/handler_payment_service_users_list.go new file mode 100644 index 000000000..37dc7bcf8 --- /dev/null +++ b/internal/api/v3/handler_payment_service_users_list.go @@ -0,0 +1,41 @@ +package v3 + +import ( + "net/http" + + "github.com/formancehq/go-libs/v3/api" + "github.com/formancehq/go-libs/v3/bun/bunpaginate" + "github.com/formancehq/go-libs/v3/pointer" + "github.com/formancehq/payments/internal/api/backend" + "github.com/formancehq/payments/internal/otel" + "github.com/formancehq/payments/internal/storage" +) + +func paymentServiceUsersList(backend backend.Backend) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + ctx, span := otel.Tracer().Start(r.Context(), "v3_paymentServiceUsersList") + defer span.End() + + query, err := bunpaginate.Extract[storage.ListPSUsQuery](r, func() (*storage.ListPSUsQuery, error) { + options, err := getPagination(span, r, storage.PSUQuery{}) + if err != nil { + return nil, err + } + return pointer.For(storage.NewListPSUQuery(*options)), nil + }) + if err != nil { + otel.RecordError(span, err) + api.BadRequest(w, ErrValidation, err) + return + } + + cursor, err := backend.PaymentServiceUsersList(ctx, *query) + if err != nil { + otel.RecordError(span, err) + handleServiceErrors(w, r, err) + return + } + + api.RenderCursor(w, *cursor) + } +} diff --git a/internal/api/v3/handler_payment_service_users_list_test.go b/internal/api/v3/handler_payment_service_users_list_test.go new file mode 100644 index 000000000..6ed636bc8 --- /dev/null +++ b/internal/api/v3/handler_payment_service_users_list_test.go @@ -0,0 +1,52 @@ +package v3 + +import ( + "fmt" + "net/http" + "net/http/httptest" + + "github.com/formancehq/go-libs/v3/bun/bunpaginate" + "github.com/formancehq/payments/internal/api/backend" + "github.com/formancehq/payments/internal/models" + . "github.com/onsi/ginkgo/v2" + "go.uber.org/mock/gomock" +) + +var _ = Describe("API v3 Payment Service Users List", func() { + var ( + handlerFn http.HandlerFunc + ) + + Context("list psu", func() { + var ( + w *httptest.ResponseRecorder + m *backend.MockBackend + ) + BeforeEach(func() { + w = httptest.NewRecorder() + ctrl := gomock.NewController(GinkgoT()) + m = backend.NewMockBackend(ctrl) + handlerFn = paymentServiceUsersList(m) + }) + + It("should return an internal server error when backend returns error", func(ctx SpecContext) { + req := httptest.NewRequest(http.MethodGet, "/", nil) + m.EXPECT().PaymentServiceUsersList(gomock.Any(), gomock.Any()).Return( + &bunpaginate.Cursor[models.PaymentServiceUser]{}, fmt.Errorf("psu list error"), + ) + handlerFn(w, req) + + assertExpectedResponse(w.Result(), http.StatusInternalServerError, "INTERNAL") + }) + + It("should return a cursor object", func(ctx SpecContext) { + req := httptest.NewRequest(http.MethodGet, "/", nil) + m.EXPECT().PaymentServiceUsersList(gomock.Any(), gomock.Any()).Return( + &bunpaginate.Cursor[models.PaymentServiceUser]{}, nil, + ) + handlerFn(w, req) + + assertExpectedResponse(w.Result(), http.StatusOK, "cursor") + }) + }) +}) diff --git a/internal/api/v3/router.go b/internal/api/v3/router.go index a10e81731..cdfc01441 100644 --- a/internal/api/v3/router.go +++ b/internal/api/v3/router.go @@ -121,7 +121,20 @@ func newRouter(backend backend.Backend, a auth.Authenticator, debug bool) *chi.M r.Get("/adjustments", paymentInitiationAdjustmentsList(backend)) r.Get("/payments", paymentInitiationPaymentsList(backend)) }) + }) + + r.Route("/payment-service-users", func(r chi.Router) { + r.Post("/", paymentServiceUsersCreate(backend, validator)) + r.Get("/", paymentServiceUsersList(backend)) + r.Route("/{paymentServiceUserID}", func(r chi.Router) { + r.Get("/", paymentServiceUsersGet(backend)) + + r.Route("/bank-accounts/{bankAccountID}", func(r chi.Router) { + r.Post("/", paymentServiceUsersAddBankAccount(backend)) + r.Post("/forward", paymentServiceUsersForwardBankAccountToConnector(backend, validator)) + }) + }) }) }) }) @@ -153,6 +166,10 @@ func bankAccountID(r *http.Request) string { return chi.URLParam(r, "bankAccountID") } +func paymentServiceUserID(r *http.Request) string { + return chi.URLParam(r, "paymentServiceUserID") +} + func scheduleID(r *http.Request) string { return chi.URLParam(r, "scheduleID") } diff --git a/internal/connectors/engine/activities/activity.go b/internal/connectors/engine/activities/activity.go index 01742570d..0e6cbfe64 100644 --- a/internal/connectors/engine/activities/activity.go +++ b/internal/connectors/engine/activities/activity.go @@ -187,6 +187,10 @@ func (a Activities) DefinitionSet() temporalworker.DefinitionSet { Name: "StorageBankAccountsGet", Func: a.StorageBankAccountsGet, }). + Append(temporalworker.Definition{ + Name: "StoragePaymentServiceUsersGet", + Func: a.StoragePaymentServiceUsersGet, + }). Append(temporalworker.Definition{ Name: "StorageBalancesDelete", Func: a.StorageBalancesDelete, diff --git a/internal/connectors/engine/activities/storage_payment_service_users_get.go b/internal/connectors/engine/activities/storage_payment_service_users_get.go new file mode 100644 index 000000000..afbbd3308 --- /dev/null +++ b/internal/connectors/engine/activities/storage_payment_service_users_get.go @@ -0,0 +1,25 @@ +package activities + +import ( + context "context" + + "github.com/formancehq/payments/internal/models" + "github.com/google/uuid" + "go.temporal.io/sdk/workflow" +) + +func (a Activities) StoragePaymentServiceUsersGet(ctx context.Context, id uuid.UUID) (*models.PaymentServiceUser, error) { + psu, err := a.storage.PaymentServiceUsersGet(ctx, id) + if err != nil { + return nil, temporalStorageError(err) + } + return psu, nil +} + +var StoragePaymentServiceUsersGetActivity = Activities{}.StoragePaymentServiceUsersGet + +func StoragePaymentServiceUsersGet(ctx workflow.Context, id uuid.UUID) (*models.PaymentServiceUser, error) { + var result models.PaymentServiceUser + err := executeActivity(ctx, StoragePaymentServiceUsersGetActivity, &result, id) + return &result, err +} diff --git a/internal/connectors/engine/engine.go b/internal/connectors/engine/engine.go index bc6964a5b..a3492bd9f 100644 --- a/internal/connectors/engine/engine.go +++ b/internal/connectors/engine/engine.go @@ -46,7 +46,7 @@ type Engine interface { // Forward a bank account to the given connector, which will create it // in the external system (PSP). - ForwardBankAccount(ctx context.Context, bankAccountID uuid.UUID, connectorID models.ConnectorID, waitResult bool) (models.Task, error) + ForwardBankAccount(ctx context.Context, ba models.BankAccount, connectorID models.ConnectorID, waitResult bool) (models.Task, error) // Create a transfer between two accounts on the given connector (PSP). CreateTransfer(ctx context.Context, piID models.PaymentInitiationID, attempt int, waitResult bool) (models.Task, error) // Reverse a transfer on the given connector (PSP). @@ -489,7 +489,7 @@ func (e *engine) CreateFormancePaymentInitiation(ctx context.Context, pi models. return nil } -func (e *engine) ForwardBankAccount(ctx context.Context, bankAccountID uuid.UUID, connectorID models.ConnectorID, waitResult bool) (models.Task, error) { +func (e *engine) ForwardBankAccount(ctx context.Context, ba models.BankAccount, connectorID models.ConnectorID, waitResult bool) (models.Task, error) { ctx, span := otel.Tracer().Start(ctx, "engine.ForwardBankAccount") defer span.End() @@ -501,15 +501,7 @@ func (e *engine) ForwardBankAccount(ctx context.Context, bankAccountID uuid.UUID return models.Task{}, err } - if _, err := e.storage.BankAccountsGet(ctx, bankAccountID, false); err != nil { - otel.RecordError(span, err) - if errors.Is(err, storage.ErrNotFound) { - return models.Task{}, fmt.Errorf("bank account %w", ErrNotFound) - } - return models.Task{}, err - } - - id := e.taskIDReferenceFor(IDPrefixBankAccountCreate, connectorID, bankAccountID.String()) + id := e.taskIDReferenceFor(IDPrefixBankAccountCreate, connectorID, ba.ID.String()) now := time.Now().UTC() task := models.Task{ ID: models.TaskID{ @@ -540,9 +532,9 @@ func (e *engine) ForwardBankAccount(ctx context.Context, bankAccountID uuid.UUID }, workflow.RunCreateBankAccount, workflow.CreateBankAccount{ - TaskID: task.ID, - ConnectorID: connectorID, - BankAccountID: bankAccountID, + TaskID: task.ID, + ConnectorID: connectorID, + BankAccount: ba, }, ) if err != nil { diff --git a/internal/connectors/engine/engine_generated.go b/internal/connectors/engine/engine_generated.go index 4beabc26e..c3c8b4431 100644 --- a/internal/connectors/engine/engine_generated.go +++ b/internal/connectors/engine/engine_generated.go @@ -158,18 +158,18 @@ func (mr *MockEngineMockRecorder) DeletePool(ctx, poolID any) *gomock.Call { } // ForwardBankAccount mocks base method. -func (m *MockEngine) ForwardBankAccount(ctx context.Context, bankAccountID uuid.UUID, connectorID models.ConnectorID, waitResult bool) (models.Task, error) { +func (m *MockEngine) ForwardBankAccount(ctx context.Context, ba models.BankAccount, connectorID models.ConnectorID, waitResult bool) (models.Task, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "ForwardBankAccount", ctx, bankAccountID, connectorID, waitResult) + ret := m.ctrl.Call(m, "ForwardBankAccount", ctx, ba, connectorID, waitResult) ret0, _ := ret[0].(models.Task) ret1, _ := ret[1].(error) return ret0, ret1 } // ForwardBankAccount indicates an expected call of ForwardBankAccount. -func (mr *MockEngineMockRecorder) ForwardBankAccount(ctx, bankAccountID, connectorID, waitResult any) *gomock.Call { +func (mr *MockEngineMockRecorder) ForwardBankAccount(ctx, ba, connectorID, waitResult any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ForwardBankAccount", reflect.TypeOf((*MockEngine)(nil).ForwardBankAccount), ctx, bankAccountID, connectorID, waitResult) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ForwardBankAccount", reflect.TypeOf((*MockEngine)(nil).ForwardBankAccount), ctx, ba, connectorID, waitResult) } // HandleWebhook mocks base method. diff --git a/internal/connectors/engine/engine_test.go b/internal/connectors/engine/engine_test.go index c7980db57..a5cbac563 100644 --- a/internal/connectors/engine/engine_test.go +++ b/internal/connectors/engine/engine_test.go @@ -299,19 +299,19 @@ var _ = Describe("Engine Tests", func() { Context("forwarding a bank account to a connector", func() { var ( - bankID uuid.UUID + ba models.BankAccount connID models.ConnectorID ) BeforeEach(func() { connID = models.ConnectorID{Reference: uuid.New(), Provider: "psp"} - bankID = uuid.New() + ba = models.BankAccount{} }) It("should return not found error when storage doesn't find connector", func(ctx SpecContext) { store.EXPECT().ConnectorsGet(gomock.Any(), connID).Return( nil, fmt.Errorf("some not found err: %w", storage.ErrNotFound), ) - _, err := eng.ForwardBankAccount(ctx, bankID, connID, false) + _, err := eng.ForwardBankAccount(ctx, ba, connID, false) Expect(err).NotTo(BeNil()) Expect(err).To(MatchError(engine.ErrNotFound)) }) @@ -319,40 +319,18 @@ var _ = Describe("Engine Tests", func() { It("should return original error when storage returns misc error from connector fetch", func(ctx SpecContext) { expectedErr := fmt.Errorf("original") store.EXPECT().ConnectorsGet(gomock.Any(), connID).Return(nil, expectedErr) - _, err := eng.ForwardBankAccount(ctx, bankID, connID, false) - Expect(err).NotTo(BeNil()) - Expect(err).To(MatchError(expectedErr)) - }) - - It("should return not found error when storage doesn't find bank account", func(ctx SpecContext) { - store.EXPECT().ConnectorsGet(gomock.Any(), connID).Return(nil, nil) - store.EXPECT().BankAccountsGet(gomock.Any(), bankID, false).Return( - nil, fmt.Errorf("some not found err: %w", storage.ErrNotFound), - ) - _, err := eng.ForwardBankAccount(ctx, bankID, connID, false) - Expect(err).NotTo(BeNil()) - Expect(err).To(MatchError(engine.ErrNotFound)) - }) - - It("should return original error when storage returns misc error from bank account fetch", func(ctx SpecContext) { - expectedErr := fmt.Errorf("original") - store.EXPECT().ConnectorsGet(gomock.Any(), connID).Return(nil, nil) - store.EXPECT().BankAccountsGet(gomock.Any(), bankID, false).Return( - nil, fmt.Errorf("some not found err: %w", expectedErr), - ) - _, err := eng.ForwardBankAccount(ctx, bankID, connID, false) + _, err := eng.ForwardBankAccount(ctx, ba, connID, false) Expect(err).NotTo(BeNil()) Expect(err).To(MatchError(expectedErr)) }) It("should return storage error when task cannot be upserted", func(ctx SpecContext) { store.EXPECT().ConnectorsGet(gomock.Any(), connID).Return(nil, nil) - store.EXPECT().BankAccountsGet(gomock.Any(), bankID, false).Return(nil, nil) expectedErr := fmt.Errorf("fffff") store.EXPECT().TasksUpsert(gomock.Any(), gomock.AssignableToTypeOf(models.Task{})).Return( expectedErr, ) - _, err := eng.ForwardBankAccount(ctx, bankID, connID, false) + _, err := eng.ForwardBankAccount(ctx, ba, connID, false) Expect(err).NotTo(BeNil()) Expect(err).To(MatchError(expectedErr)) }) @@ -361,7 +339,6 @@ var _ = Describe("Engine Tests", func() { store.EXPECT().ConnectorsGet(gomock.Any(), connID).Return( &models.Connector{ID: connID}, nil, ) - store.EXPECT().BankAccountsGet(gomock.Any(), bankID, false).Return(nil, nil) store.EXPECT().TasksUpsert(gomock.Any(), gomock.AssignableToTypeOf(models.Task{})).Return(nil) expectedErr := fmt.Errorf("workflow failed") cl.EXPECT().ExecuteWorkflow(gomock.Any(), WithWorkflowOptions(engine.IDPrefixBankAccountCreate, defaultTaskQueue), @@ -369,7 +346,7 @@ var _ = Describe("Engine Tests", func() { gomock.AssignableToTypeOf(workflow.CreateBankAccount{}), ).Return(nil, expectedErr) - _, err := eng.ForwardBankAccount(ctx, bankID, connID, false) + _, err := eng.ForwardBankAccount(ctx, ba, connID, false) Expect(err).NotTo(BeNil()) Expect(err).To(MatchError(expectedErr)) }) @@ -378,14 +355,13 @@ var _ = Describe("Engine Tests", func() { store.EXPECT().ConnectorsGet(gomock.Any(), connID).Return( &models.Connector{ID: connID}, nil, ) - store.EXPECT().BankAccountsGet(gomock.Any(), bankID, false).Return(nil, nil) store.EXPECT().TasksUpsert(gomock.Any(), gomock.AssignableToTypeOf(models.Task{})).Return(nil) cl.EXPECT().ExecuteWorkflow(gomock.Any(), WithWorkflowOptions(engine.IDPrefixBankAccountCreate, defaultTaskQueue), workflow.RunCreateBankAccount, gomock.AssignableToTypeOf(workflow.CreateBankAccount{}), ).Return(nil, nil) - task, err := eng.ForwardBankAccount(ctx, bankID, connID, false) + task, err := eng.ForwardBankAccount(ctx, ba, connID, false) Expect(err).To(BeNil()) Expect(task.ID.Reference).To(ContainSubstring(engine.IDPrefixBankAccountCreate)) Expect(task.ID.Reference).To(ContainSubstring(stackName)) diff --git a/internal/connectors/engine/workflow/create_bank_account.go b/internal/connectors/engine/workflow/create_bank_account.go index 100742123..a5194c1d6 100644 --- a/internal/connectors/engine/workflow/create_bank_account.go +++ b/internal/connectors/engine/workflow/create_bank_account.go @@ -3,16 +3,15 @@ package workflow import ( "github.com/formancehq/payments/internal/connectors/engine/activities" "github.com/formancehq/payments/internal/models" - "github.com/google/uuid" "go.temporal.io/api/enums/v1" "go.temporal.io/sdk/temporal" "go.temporal.io/sdk/workflow" ) type CreateBankAccount struct { - TaskID models.TaskID - ConnectorID models.ConnectorID - BankAccountID uuid.UUID + TaskID models.TaskID + ConnectorID models.ConnectorID + BankAccount models.BankAccount } func (w Workflow) runCreateBankAccount( @@ -45,20 +44,13 @@ func (w Workflow) createBankAccount( ctx workflow.Context, createBankAccount CreateBankAccount, ) (string, error) { - bankAccount, err := activities.StorageBankAccountsGet( - infiniteRetryContext(ctx), - createBankAccount.BankAccountID, - true, - ) - if err != nil { - return "", err - } + bankAccount := createBankAccount.BankAccount createBAResponse, err := activities.PluginCreateBankAccount( infiniteRetryContext(ctx), createBankAccount.ConnectorID, models.CreateBankAccountRequest{ - BankAccount: *bankAccount, + BankAccount: bankAccount, }, ) if err != nil { @@ -93,7 +85,7 @@ func (w Workflow) createBankAccount( err = activities.StorageBankAccountsAddRelatedAccount( infiniteRetryContext(ctx), - createBankAccount.BankAccountID, + createBankAccount.BankAccount.ID, relatedAccount, ) if err != nil { @@ -115,7 +107,7 @@ func (w Workflow) createBankAccount( ), RunSendEvents, SendEvents{ - BankAccount: bankAccount, + BankAccount: &bankAccount, }, ).Get(ctx, nil); err != nil { return "", err diff --git a/internal/connectors/engine/workflow/create_bank_account_test.go b/internal/connectors/engine/workflow/create_bank_account_test.go index 88052c49f..ac2f74e60 100644 --- a/internal/connectors/engine/workflow/create_bank_account_test.go +++ b/internal/connectors/engine/workflow/create_bank_account_test.go @@ -13,7 +13,6 @@ import ( ) func (s *UnitTestSuite) Test_CreateBankAccount_Success() { - s.env.OnActivity(activities.StorageBankAccountsGetActivity, mock.Anything, s.bankAccount.ID, true).Once().Return(&s.bankAccount, nil) s.env.OnActivity(activities.PluginCreateBankAccountActivity, mock.Anything, mock.Anything).Once().Return(func(ctx context.Context, request activities.CreateBankAccountRequest) (*models.CreateBankAccountResponse, error) { s.Equal(s.connectorID, request.ConnectorID) s.Equal(s.bankAccount.ID, request.Req.BankAccount.ID) @@ -52,40 +51,15 @@ func (s *UnitTestSuite) Test_CreateBankAccount_Success() { Reference: "test", ConnectorID: s.connectorID, }, - ConnectorID: s.connectorID, - BankAccountID: s.bankAccount.ID, + ConnectorID: s.connectorID, + BankAccount: s.bankAccount, }) s.True(s.env.IsWorkflowCompleted()) s.NoError(s.env.GetWorkflowError()) } -func (s *UnitTestSuite) Test_CreateBankAccount_StorageBankAccountGet_Error() { - s.env.OnActivity(activities.StorageBankAccountsGetActivity, mock.Anything, s.bankAccount.ID, true).Once().Return( - nil, - temporal.NewNonRetryableApplicationError("test", "test", errors.New("test")), - ) - s.env.OnActivity(activities.StorageTasksStoreActivity, mock.Anything, mock.Anything).Once().Return(func(ctx context.Context, task models.Task) error { - s.Equal(models.TASK_STATUS_FAILED, task.Status) - return nil - }) - - s.env.ExecuteWorkflow(RunCreateBankAccount, CreateBankAccount{ - TaskID: models.TaskID{ - Reference: "test", - ConnectorID: s.connectorID, - }, - ConnectorID: s.connectorID, - BankAccountID: s.bankAccount.ID, - }) - - s.True(s.env.IsWorkflowCompleted()) - err := s.env.GetWorkflowError() - s.Error(err) -} - func (s *UnitTestSuite) Test_CreateBankAccount_PluginCreateBankAccount_Error() { - s.env.OnActivity(activities.StorageBankAccountsGetActivity, mock.Anything, s.bankAccount.ID, true).Once().Return(&s.bankAccount, nil) s.env.OnActivity(activities.PluginCreateBankAccountActivity, mock.Anything, mock.Anything).Once().Return( nil, temporal.NewNonRetryableApplicationError("test", "test", errors.New("test")), @@ -100,8 +74,8 @@ func (s *UnitTestSuite) Test_CreateBankAccount_PluginCreateBankAccount_Error() { Reference: "test", ConnectorID: s.connectorID, }, - ConnectorID: s.connectorID, - BankAccountID: s.bankAccount.ID, + ConnectorID: s.connectorID, + BankAccount: s.bankAccount, }) s.True(s.env.IsWorkflowCompleted()) @@ -110,7 +84,6 @@ func (s *UnitTestSuite) Test_CreateBankAccount_PluginCreateBankAccount_Error() { } func (s *UnitTestSuite) Test_CreateBankAccount_StorageAccountsStore_Error() { - s.env.OnActivity(activities.StorageBankAccountsGetActivity, mock.Anything, s.bankAccount.ID, true).Once().Return(&s.bankAccount, nil) s.env.OnActivity(activities.PluginCreateBankAccountActivity, mock.Anything, mock.Anything).Once().Return(&models.CreateBankAccountResponse{ RelatedAccount: s.pspAccount, }, nil) @@ -127,8 +100,8 @@ func (s *UnitTestSuite) Test_CreateBankAccount_StorageAccountsStore_Error() { Reference: "test", ConnectorID: s.connectorID, }, - ConnectorID: s.connectorID, - BankAccountID: s.bankAccount.ID, + ConnectorID: s.connectorID, + BankAccount: s.bankAccount, }) s.True(s.env.IsWorkflowCompleted()) @@ -137,7 +110,6 @@ func (s *UnitTestSuite) Test_CreateBankAccount_StorageAccountsStore_Error() { } func (s *UnitTestSuite) Test_CreateBankAccount_StorageBankAccountsAddRelatedAccount_Error() { - s.env.OnActivity(activities.StorageBankAccountsGetActivity, mock.Anything, s.bankAccount.ID, true).Once().Return(&s.bankAccount, nil) s.env.OnActivity(activities.PluginCreateBankAccountActivity, mock.Anything, mock.Anything).Once().Return(&models.CreateBankAccountResponse{ RelatedAccount: s.pspAccount, }, nil) @@ -155,8 +127,8 @@ func (s *UnitTestSuite) Test_CreateBankAccount_StorageBankAccountsAddRelatedAcco Reference: "test", ConnectorID: s.connectorID, }, - ConnectorID: s.connectorID, - BankAccountID: s.bankAccount.ID, + ConnectorID: s.connectorID, + BankAccount: s.bankAccount, }) s.True(s.env.IsWorkflowCompleted()) @@ -165,7 +137,6 @@ func (s *UnitTestSuite) Test_CreateBankAccount_StorageBankAccountsAddRelatedAcco } func (s *UnitTestSuite) Test_CreateBankAccount_RunSendEvents_Error() { - s.env.OnActivity(activities.StorageBankAccountsGetActivity, mock.Anything, s.bankAccount.ID, true).Once().Return(&s.bankAccount, nil) s.env.OnActivity(activities.PluginCreateBankAccountActivity, mock.Anything, mock.Anything).Once().Return(&models.CreateBankAccountResponse{ RelatedAccount: s.pspAccount, }, nil) @@ -182,8 +153,8 @@ func (s *UnitTestSuite) Test_CreateBankAccount_RunSendEvents_Error() { Reference: "test", ConnectorID: s.connectorID, }, - ConnectorID: s.connectorID, - BankAccountID: s.bankAccount.ID, + ConnectorID: s.connectorID, + BankAccount: s.bankAccount, }) s.True(s.env.IsWorkflowCompleted()) @@ -192,7 +163,6 @@ func (s *UnitTestSuite) Test_CreateBankAccount_RunSendEvents_Error() { } func (s *UnitTestSuite) Test_CreateBankAccount_StorageTasksStore_Error() { - s.env.OnActivity(activities.StorageBankAccountsGetActivity, mock.Anything, s.bankAccount.ID, true).Once().Return(&s.bankAccount, nil) s.env.OnActivity(activities.PluginCreateBankAccountActivity, mock.Anything, mock.Anything).Once().Return(&models.CreateBankAccountResponse{ RelatedAccount: s.pspAccount, }, nil) @@ -209,8 +179,8 @@ func (s *UnitTestSuite) Test_CreateBankAccount_StorageTasksStore_Error() { Reference: "test", ConnectorID: s.connectorID, }, - ConnectorID: s.connectorID, - BankAccountID: s.bankAccount.ID, + ConnectorID: s.connectorID, + BankAccount: s.bankAccount, }) s.True(s.env.IsWorkflowCompleted()) diff --git a/internal/connectors/engine/workflow/main_test.go b/internal/connectors/engine/workflow/main_test.go index 5053ec4e9..09bbf8a45 100644 --- a/internal/connectors/engine/workflow/main_test.go +++ b/internal/connectors/engine/workflow/main_test.go @@ -33,6 +33,7 @@ type UnitTestSuite struct { connector models.Connector bankAccount models.BankAccount + paymentServiceUser models.PaymentServiceUser paymentPayout models.Payment paymentWithAdjustmentAmount models.Payment account models.Account @@ -120,6 +121,30 @@ func (s *UnitTestSuite) addData() { }, } + s.paymentServiceUser = models.PaymentServiceUser{ + ID: uuid.New(), + Name: "test", + CreatedAt: now, + ContactDetails: &models.ContactDetails{ + Email: pointer.For("test"), + PhoneNumber: pointer.For("test"), + }, + Address: &models.Address{ + StreetName: pointer.For("test"), + StreetNumber: pointer.For("test"), + City: pointer.For("test"), + Region: pointer.For("test"), + PostalCode: pointer.For("test"), + Country: pointer.For("test"), + }, + BankAccountIDs: []uuid.UUID{ + s.bankAccount.ID, + }, + Metadata: map[string]string{ + "foo": "bar", + }, + } + s.paymentPayout = models.Payment{ ID: s.paymentPayoutID, ConnectorID: s.connectorID, diff --git a/internal/connectors/plugins/public/bankingcircle/payouts.go b/internal/connectors/plugins/public/bankingcircle/payouts.go index 5cb99042e..f1ab40620 100644 --- a/internal/connectors/plugins/public/bankingcircle/payouts.go +++ b/internal/connectors/plugins/public/bankingcircle/payouts.go @@ -33,8 +33,8 @@ func (p *Plugin) validatePayoutRequest(pi models.PSPPaymentInitiation) error { ) } - if pi.DestinationAccount.Metadata[models.BankAccountAccountNumberMetadataKey] == "" && - pi.DestinationAccount.Metadata[models.BankAccountIBANMetadataKey] == "" { + if pi.DestinationAccount.Metadata[models.AccountAccountNumberMetadataKey] == "" && + pi.DestinationAccount.Metadata[models.AccountIBANMetadataKey] == "" { return errorsutils.NewWrappedError( fmt.Errorf("destination account number or IBAN is required in payout request"), models.ErrInvalidRequest, @@ -80,9 +80,9 @@ func (p *Plugin) createPayout(ctx context.Context, pi models.PSPPaymentInitiatio ) } - account := pi.DestinationAccount.Metadata[models.BankAccountAccountNumberMetadataKey] + account := pi.DestinationAccount.Metadata[models.AccountAccountNumberMetadataKey] if account == "" { - account = pi.DestinationAccount.Metadata[models.BankAccountIBANMetadataKey] + account = pi.DestinationAccount.Metadata[models.AccountIBANMetadataKey] } resp, err := p.client.InitiateTransferOrPayouts(ctx, &client.PaymentRequest{ @@ -102,8 +102,8 @@ func (p *Plugin) createPayout(ctx context.Context, pi models.PSPPaymentInitiatio ChargeBearer: "SHA", CreditorAccount: &client.PaymentAccount{ Account: account, - FinancialInstitution: pi.DestinationAccount.Metadata[models.BankAccountSwiftBicCodeMetadataKey], - Country: pi.DestinationAccount.Metadata[models.BankAccountCountryMetadataKey], + FinancialInstitution: pi.DestinationAccount.Metadata[models.AccountSwiftBicCodeMetadataKey], + Country: pi.DestinationAccount.Metadata[models.AccountBankAccountCountryMetadataKey], }, CreditorName: *pi.DestinationAccount.Name, }) diff --git a/internal/connectors/plugins/public/bankingcircle/payouts_test.go b/internal/connectors/plugins/public/bankingcircle/payouts_test.go index 4805658b8..2fb9a7b7d 100644 --- a/internal/connectors/plugins/public/bankingcircle/payouts_test.go +++ b/internal/connectors/plugins/public/bankingcircle/payouts_test.go @@ -53,10 +53,10 @@ var _ = Describe("BankingCircle Plugin Payouts Creation", func() { Name: pointer.For("acc2"), DefaultAsset: pointer.For("EUR/2"), Metadata: map[string]string{ - models.BankAccountIBANMetadataKey: "iban", - models.BankAccountAccountNumberMetadataKey: "acc", - models.BankAccountSwiftBicCodeMetadataKey: "bic", - models.BankAccountCountryMetadataKey: "US", + models.AccountIBANMetadataKey: "iban", + models.AccountAccountNumberMetadataKey: "acc", + models.AccountSwiftBicCodeMetadataKey: "bic", + models.AccountBankAccountCountryMetadataKey: "US", }, }, Amount: big.NewInt(100), From be356b0dd77e8a46838685e1554a9438014bcefd Mon Sep 17 00:00:00 2001 From: Paul Nicolas Date: Fri, 25 Apr 2025 12:22:46 +0200 Subject: [PATCH 05/13] feat(tests): update sdks and add psu integration tests --- docs/api/README.md | 682 ++++++++++ openapi.yaml | 301 +++++ openapi/v3/v3-api.yaml | 142 +++ openapi/v3/v3-parameters.yaml | 8 + openapi/v3/v3-schemas.yaml | 171 +++ pkg/client/.speakeasy/gen.lock | 88 +- pkg/client/README.md | 5 + .../docs/models/components/v3address.md | 13 + .../models/components/v3addressrequest.md | 13 + .../models/components/v3contactdetails.md | 9 + .../components/v3contactdetailsrequest.md | 9 + .../v3createpaymentserviceuserrequest.md | 12 + .../v3createpaymentserviceuserresponse.md | 8 + ...ardpaymentserviceuserbankaccountrequest.md | 8 + ...rdpaymentserviceuserbankaccountresponse.md | 8 + ...ymentserviceuserbankaccountresponsedata.md | 8 + .../v3getpaymentserviceuserresponse.md | 8 + .../models/components/v3paymentserviceuser.md | 14 + .../v3paymentserviceuserscursorresponse.md | 8 + ...paymentserviceuserscursorresponsecursor.md | 12 + ...dbankaccounttopaymentserviceuserrequest.md | 9 + ...bankaccounttopaymentserviceuserresponse.md | 9 + .../v3createpaymentserviceuserresponse.md | 10 + ...ardpaymentserviceuserbankaccountrequest.md | 10 + ...rdpaymentserviceuserbankaccountresponse.md | 10 + .../v3getpaymentserviceuserrequest.md | 8 + .../v3getpaymentserviceuserresponse.md | 10 + .../v3listpaymentserviceusersrequest.md | 10 + .../v3listpaymentserviceusersresponse.md | 10 + pkg/client/docs/sdks/v3/README.md | 270 ++++ pkg/client/models/components/v3address.go | 54 + .../models/components/v3addressrequest.go | 54 + .../models/components/v3contactdetails.go | 22 + .../components/v3contactdetailsrequest.go | 22 + .../v3createpaymentserviceuserrequest.go | 46 + .../v3createpaymentserviceuserresponse.go | 15 + ...ardpaymentserviceuserbankaccountrequest.go | 14 + ...rdpaymentserviceuserbankaccountresponse.go | 27 + .../v3getpaymentserviceuserresponse.go | 14 + .../models/components/v3paymentserviceuser.go | 78 ++ .../v3paymentserviceuserscursorresponse.go | 57 + .../v3addbankaccounttopaymentserviceuser.go | 48 + .../operations/v3createpaymentserviceuser.go | 36 + .../v3forwardpaymentserviceuserbankaccount.go | 65 + .../operations/v3getpaymentserviceuser.go | 48 + .../operations/v3listpaymentserviceusers.go | 66 + pkg/client/v3.go | 1106 +++++++++++++++++ test/e2e/api_payment_service_users_test.go | 254 ++++ 48 files changed, 3908 insertions(+), 1 deletion(-) create mode 100644 pkg/client/docs/models/components/v3address.md create mode 100644 pkg/client/docs/models/components/v3addressrequest.md create mode 100644 pkg/client/docs/models/components/v3contactdetails.md create mode 100644 pkg/client/docs/models/components/v3contactdetailsrequest.md create mode 100644 pkg/client/docs/models/components/v3createpaymentserviceuserrequest.md create mode 100644 pkg/client/docs/models/components/v3createpaymentserviceuserresponse.md create mode 100644 pkg/client/docs/models/components/v3forwardpaymentserviceuserbankaccountrequest.md create mode 100644 pkg/client/docs/models/components/v3forwardpaymentserviceuserbankaccountresponse.md create mode 100644 pkg/client/docs/models/components/v3forwardpaymentserviceuserbankaccountresponsedata.md create mode 100644 pkg/client/docs/models/components/v3getpaymentserviceuserresponse.md create mode 100644 pkg/client/docs/models/components/v3paymentserviceuser.md create mode 100644 pkg/client/docs/models/components/v3paymentserviceuserscursorresponse.md create mode 100644 pkg/client/docs/models/components/v3paymentserviceuserscursorresponsecursor.md create mode 100644 pkg/client/docs/models/operations/v3addbankaccounttopaymentserviceuserrequest.md create mode 100644 pkg/client/docs/models/operations/v3addbankaccounttopaymentserviceuserresponse.md create mode 100644 pkg/client/docs/models/operations/v3createpaymentserviceuserresponse.md create mode 100644 pkg/client/docs/models/operations/v3forwardpaymentserviceuserbankaccountrequest.md create mode 100644 pkg/client/docs/models/operations/v3forwardpaymentserviceuserbankaccountresponse.md create mode 100644 pkg/client/docs/models/operations/v3getpaymentserviceuserrequest.md create mode 100644 pkg/client/docs/models/operations/v3getpaymentserviceuserresponse.md create mode 100644 pkg/client/docs/models/operations/v3listpaymentserviceusersrequest.md create mode 100644 pkg/client/docs/models/operations/v3listpaymentserviceusersresponse.md create mode 100644 pkg/client/models/components/v3address.go create mode 100644 pkg/client/models/components/v3addressrequest.go create mode 100644 pkg/client/models/components/v3contactdetails.go create mode 100644 pkg/client/models/components/v3contactdetailsrequest.go create mode 100644 pkg/client/models/components/v3createpaymentserviceuserrequest.go create mode 100644 pkg/client/models/components/v3createpaymentserviceuserresponse.go create mode 100644 pkg/client/models/components/v3forwardpaymentserviceuserbankaccountrequest.go create mode 100644 pkg/client/models/components/v3forwardpaymentserviceuserbankaccountresponse.go create mode 100644 pkg/client/models/components/v3getpaymentserviceuserresponse.go create mode 100644 pkg/client/models/components/v3paymentserviceuser.go create mode 100644 pkg/client/models/components/v3paymentserviceuserscursorresponse.go create mode 100644 pkg/client/models/operations/v3addbankaccounttopaymentserviceuser.go create mode 100644 pkg/client/models/operations/v3createpaymentserviceuser.go create mode 100644 pkg/client/models/operations/v3forwardpaymentserviceuserbankaccount.go create mode 100644 pkg/client/models/operations/v3getpaymentserviceuser.go create mode 100644 pkg/client/models/operations/v3listpaymentserviceusers.go create mode 100644 test/e2e/api_payment_service_users_test.go diff --git a/docs/api/README.md b/docs/api/README.md index 21e8fbbec..14f9dfcec 100644 --- a/docs/api/README.md +++ b/docs/api/README.md @@ -2182,6 +2182,332 @@ To perform this operation, you must be authenticated by means of one of the foll None ( Scopes: payments:read ) +## Create a formance payment service user object + + + +> Code samples + +```http +POST /v3/payment-service-users HTTP/1.1 + +Content-Type: application/json +Accept: application/json + +``` + +`POST /v3/payment-service-users` + +> Body parameter + +```json +{ + "name": "string", + "contactDetails": { + "email": "string", + "phoneNuber": "string" + }, + "address": { + "streetNumber": "string", + "streetName": "string", + "city": "string", + "region": "string", + "postalCode": "string", + "country": "string" + }, + "bankAccountIDs": [ + "string" + ], + "metadata": { + "property1": "string", + "property2": "string" + } +} +``` + +

Parameters

+ +|Name|In|Type|Required|Description| +|---|---|---|---|---| +|body|body|[V3CreatePaymentServiceUserRequest](#schemav3createpaymentserviceuserrequest)|false|none| + +> Example responses + +> 201 Response + +```json +{ + "data": "string" +} +``` + +

Responses

+ +|Status|Meaning|Description|Schema| +|---|---|---|---| +|201|[Created](https://tools.ietf.org/html/rfc7231#section-6.3.2)|Created|[V3CreatePaymentServiceUserResponse](#schemav3createpaymentserviceuserresponse)| +|default|Default|Error|[V3ErrorResponse](#schemav3errorresponse)| + + + +## List all payment service users + + + +> Code samples + +```http +GET /v3/payment-service-users HTTP/1.1 + +Content-Type: application/json +Accept: application/json + +``` + +`GET /v3/payment-service-users` + +> Body parameter + +```json +{} +``` + +

Parameters

+ +|Name|In|Type|Required|Description| +|---|---|---|---|---| +|pageSize|query|integer(int64)|false|The number of items to return| +|cursor|query|string|false|Parameter used in pagination requests. Set to the value of next for the next page of results. Set to the value of previous for the previous page of results. No other parameters can be set when this parameter is set.| +|body|body|[V3QueryBuilder](#schemav3querybuilder)|false|none| + +#### Detailed descriptions + +**cursor**: Parameter used in pagination requests. Set to the value of next for the next page of results. Set to the value of previous for the previous page of results. No other parameters can be set when this parameter is set. + +> Example responses + +> 200 Response + +```json +{ + "cursor": { + "pageSize": 15, + "hasMore": false, + "previous": "YXVsdCBhbmQgYSBtYXhpbXVtIG1heF9yZXN1bHRzLol=", + "next": "YXVsdCBhbmQgYSBtYXhpbXVtIG1heF9yZXN1bHRzLol=", + "data": [ + { + "id": "string", + "name": "string", + "createdAt": "2019-08-24T14:15:22Z", + "contactDetails": { + "email": "string", + "phoneNumber": "string" + }, + "address": { + "streetNumber": "string", + "streetName": "string", + "city": "string", + "region": "string", + "postalCode": "string", + "country": "string" + }, + "bankAccountIDs": [ + "string" + ], + "metadata": { + "property1": "string", + "property2": "string" + } + } + ] + } +} +``` + +

Responses

+ +|Status|Meaning|Description|Schema| +|---|---|---|---| +|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|OK|[V3PaymentServiceUsersCursorResponse](#schemav3paymentserviceuserscursorresponse)| +|default|Default|Error|[V3ErrorResponse](#schemav3errorresponse)| + + + +## Get a payment service user by ID + + + +> Code samples + +```http +GET /v3/payment-service-users/{paymentServiceUserID} HTTP/1.1 + +Accept: application/json + +``` + +`GET /v3/payment-service-users/{paymentServiceUserID}` + +

Parameters

+ +|Name|In|Type|Required|Description| +|---|---|---|---|---| +|paymentServiceUserID|path|string|true|The payment service user ID| + +> Example responses + +> 200 Response + +```json +{ + "data": { + "id": "string", + "name": "string", + "createdAt": "2019-08-24T14:15:22Z", + "contactDetails": { + "email": "string", + "phoneNumber": "string" + }, + "address": { + "streetNumber": "string", + "streetName": "string", + "city": "string", + "region": "string", + "postalCode": "string", + "country": "string" + }, + "bankAccountIDs": [ + "string" + ], + "metadata": { + "property1": "string", + "property2": "string" + } + } +} +``` + +

Responses

+ +|Status|Meaning|Description|Schema| +|---|---|---|---| +|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|OK|[V3GetPaymentServiceUserResponse](#schemav3getpaymentserviceuserresponse)| +|default|Default|Error|[V3ErrorResponse](#schemav3errorresponse)| + + + +## Add a bank account to a payment service user + + + +> Code samples + +```http +POST /v3/payment-service-users/{paymentServiceUserID}/bank-accounts/{bankAccountID} HTTP/1.1 + +Accept: application/json + +``` + +`POST /v3/payment-service-users/{paymentServiceUserID}/bank-accounts/{bankAccountID}` + +

Parameters

+ +|Name|In|Type|Required|Description| +|---|---|---|---|---| +|paymentServiceUserID|path|string|true|The payment service user ID| +|bankAccountID|path|string|true|The bank account ID| + +> Example responses + +> default Response + +```json +{ + "errorCode": "VALIDATION", + "errorMessage": "[VALIDATION] missing required config field: pollingPeriod", + "details": "string" +} +``` + +

Responses

+ +|Status|Meaning|Description|Schema| +|---|---|---|---| +|204|[No Content](https://tools.ietf.org/html/rfc7231#section-6.3.5)|No Content|None| +|default|Default|Error|[V3ErrorResponse](#schemav3errorresponse)| + + + +## Forward a payment service user's bank account to a connector + + + +> Code samples + +```http +POST /v3/payment-service-users/{paymentServiceUserID}/bank-accounts/{bankAccountID}/forward HTTP/1.1 + +Content-Type: application/json +Accept: application/json + +``` + +`POST /v3/payment-service-users/{paymentServiceUserID}/bank-accounts/{bankAccountID}/forward` + +> Body parameter + +```json +{ + "connectorID": "string" +} +``` + +

Parameters

+ +|Name|In|Type|Required|Description| +|---|---|---|---|---| +|paymentServiceUserID|path|string|true|The payment service user ID| +|bankAccountID|path|string|true|The bank account ID| +|body|body|[V3ForwardPaymentServiceUserBankAccountRequest](#schemav3forwardpaymentserviceuserbankaccountrequest)|false|none| + +> Example responses + +> 202 Response + +```json +{ + "data": { + "taskID": "string" + } +} +``` + +

Responses

+ +|Status|Meaning|Description|Schema| +|---|---|---|---| +|202|[Accepted](https://tools.ietf.org/html/rfc7231#section-6.3.3)|Accepted|[V3ForwardPaymentServiceUserBankAccountResponse](#schemav3forwardpaymentserviceuserbankaccountresponse)| +|default|Default|Error|[V3ErrorResponse](#schemav3errorresponse)| + + + ## Create a formance pool object @@ -4571,6 +4897,362 @@ None ( Scopes: payments:read ) |*anonymous*|TRANSFER| |*anonymous*|PAYOUT| +

V3CreatePaymentServiceUserRequest

+ + + + + + +```json +{ + "name": "string", + "contactDetails": { + "email": "string", + "phoneNuber": "string" + }, + "address": { + "streetNumber": "string", + "streetName": "string", + "city": "string", + "region": "string", + "postalCode": "string", + "country": "string" + }, + "bankAccountIDs": [ + "string" + ], + "metadata": { + "property1": "string", + "property2": "string" + } +} + +``` + +### Properties + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|name|string|true|none|none| +|contactDetails|[V3ContactDetailsRequest](#schemav3contactdetailsrequest)|false|none|none| +|address|[V3AddressRequest](#schemav3addressrequest)|false|none|none| +|bankAccountIDs|[string]¦null|false|none|none| +|metadata|[V3Metadata](#schemav3metadata)|false|none|none| + +

V3AddressRequest

+ + + + + + +```json +{ + "streetNumber": "string", + "streetName": "string", + "city": "string", + "region": "string", + "postalCode": "string", + "country": "string" +} + +``` + +### Properties + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|streetNumber|string|false|none|none| +|streetName|string|false|none|none| +|city|string|false|none|none| +|region|string|false|none|none| +|postalCode|string|false|none|none| +|country|string|false|none|none| + +

V3ContactDetailsRequest

+ + + + + + +```json +{ + "email": "string", + "phoneNuber": "string" +} + +``` + +### Properties + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|email|string|false|none|none| +|phoneNuber|string|false|none|none| + +

V3CreatePaymentServiceUserResponse

+ + + + + + +```json +{ + "data": "string" +} + +``` + +### Properties + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|data|string|true|none|The ID of the created payment service user| + +

V3PaymentServiceUsersCursorResponse

+ + + + + + +```json +{ + "cursor": { + "pageSize": 15, + "hasMore": false, + "previous": "YXVsdCBhbmQgYSBtYXhpbXVtIG1heF9yZXN1bHRzLol=", + "next": "YXVsdCBhbmQgYSBtYXhpbXVtIG1heF9yZXN1bHRzLol=", + "data": [ + { + "id": "string", + "name": "string", + "createdAt": "2019-08-24T14:15:22Z", + "contactDetails": { + "email": "string", + "phoneNumber": "string" + }, + "address": { + "streetNumber": "string", + "streetName": "string", + "city": "string", + "region": "string", + "postalCode": "string", + "country": "string" + }, + "bankAccountIDs": [ + "string" + ], + "metadata": { + "property1": "string", + "property2": "string" + } + } + ] + } +} + +``` + +### Properties + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|cursor|object|true|none|none| +|» pageSize|integer(int64)|true|none|none| +|» hasMore|boolean|true|none|none| +|» previous|string|false|none|none| +|» next|string|false|none|none| +|» data|[[V3PaymentServiceUser](#schemav3paymentserviceuser)]|true|none|none| + +

V3PaymentServiceUser

+ + + + + + +```json +{ + "id": "string", + "name": "string", + "createdAt": "2019-08-24T14:15:22Z", + "contactDetails": { + "email": "string", + "phoneNumber": "string" + }, + "address": { + "streetNumber": "string", + "streetName": "string", + "city": "string", + "region": "string", + "postalCode": "string", + "country": "string" + }, + "bankAccountIDs": [ + "string" + ], + "metadata": { + "property1": "string", + "property2": "string" + } +} + +``` + +### Properties + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|id|string|true|none|none| +|name|string|true|none|none| +|createdAt|string(date-time)|true|none|none| +|contactDetails|[V3ContactDetails](#schemav3contactdetails)|false|none|none| +|address|[V3Address](#schemav3address)|false|none|none| +|bankAccountIDs|[string]¦null|false|none|none| +|metadata|[V3Metadata](#schemav3metadata)|false|none|none| + +

V3ContactDetails

+ + + + + + +```json +{ + "email": "string", + "phoneNumber": "string" +} + +``` + +### Properties + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|email|string|false|none|none| +|phoneNumber|string|false|none|none| + +

V3Address

+ + + + + + +```json +{ + "streetNumber": "string", + "streetName": "string", + "city": "string", + "region": "string", + "postalCode": "string", + "country": "string" +} + +``` + +### Properties + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|streetNumber|string|false|none|none| +|streetName|string|false|none|none| +|city|string|false|none|none| +|region|string|false|none|none| +|postalCode|string|false|none|none| +|country|string|false|none|none| + +

V3GetPaymentServiceUserResponse

+ + + + + + +```json +{ + "data": { + "id": "string", + "name": "string", + "createdAt": "2019-08-24T14:15:22Z", + "contactDetails": { + "email": "string", + "phoneNumber": "string" + }, + "address": { + "streetNumber": "string", + "streetName": "string", + "city": "string", + "region": "string", + "postalCode": "string", + "country": "string" + }, + "bankAccountIDs": [ + "string" + ], + "metadata": { + "property1": "string", + "property2": "string" + } + } +} + +``` + +### Properties + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|data|[V3PaymentServiceUser](#schemav3paymentserviceuser)|true|none|none| + +

V3ForwardPaymentServiceUserBankAccountRequest

+ + + + + + +```json +{ + "connectorID": "string" +} + +``` + +### Properties + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|connectorID|string(byte)|true|none|none| + +

V3ForwardPaymentServiceUserBankAccountResponse

+ + + + + + +```json +{ + "data": { + "taskID": "string" + } +} + +``` + +### Properties + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|data|object|true|none|none| +|» taskID|string|true|none|Since this call is asynchronous, the response will contain the ID of the task that was created to forward the bank account to the PSP. You can use the task API to check the status of the task and get the resulting bank account ID.| +

V3CreatePoolRequest

diff --git a/openapi.yaml b/openapi.yaml index c8c0f7b4d..7d570e0c5 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -1699,6 +1699,142 @@ paths: security: - Authorization: - payments:read + /v3/payment-service-users: + post: + tags: + - payments.v3 + summary: Create a formance payment service user object + operationId: v3CreatePaymentServiceUser + x-speakeasy-name-override: CreatePaymentServiceUser + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/V3CreatePaymentServiceUserRequest' + responses: + "201": + description: Created + content: + application/json: + schema: + $ref: '#/components/schemas/V3CreatePaymentServiceUserResponse' + default: + description: Error + content: + application/json: + schema: + $ref: '#/components/schemas/V3ErrorResponse' + security: + - Authorization: + - payments:write + get: + tags: + - payments.v3 + summary: List all payment service users + operationId: v3ListPaymentServiceUsers + x-speakeasy-name-override: ListPaymentServiceUsers + parameters: + - $ref: '#/components/parameters/V3PageSize' + - $ref: '#/components/parameters/V3Cursor' + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/V3QueryBuilder' + responses: + "200": + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/V3PaymentServiceUsersCursorResponse' + default: + description: Error + content: + application/json: + schema: + $ref: '#/components/schemas/V3ErrorResponse' + security: + - Authorization: + - payments:read + /v3/payment-service-users/{paymentServiceUserID}: + get: + tags: + - payments.v3 + summary: Get a payment service user by ID + operationId: v3GetPaymentServiceUser + x-speakeasy-name-override: GetPaymentServiceUser + parameters: + - $ref: '#/components/parameters/V3PaymentServiceUserID' + responses: + "200": + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/V3GetPaymentServiceUserResponse' + default: + description: Error + content: + application/json: + schema: + $ref: '#/components/schemas/V3ErrorResponse' + security: + - Authorization: + - payments:read + /v3/payment-service-users/{paymentServiceUserID}/bank-accounts/{bankAccountID}: + post: + tags: + - payments.v3 + summary: Add a bank account to a payment service user + operationId: v3AddBankAccountToPaymentServiceUser + x-speakeasy-name-override: AddBankAccountToPaymentServiceUser + parameters: + - $ref: '#/components/parameters/V3PaymentServiceUserID' + - $ref: '#/components/parameters/V3BankAccountID' + responses: + "204": + description: No Content + default: + description: Error + content: + application/json: + schema: + $ref: '#/components/schemas/V3ErrorResponse' + security: + - Authorization: + - payments:write + /v3/payment-service-users/{paymentServiceUserID}/bank-accounts/{bankAccountID}/forward: + post: + tags: + - payments.v3 + summary: Forward a payment service user's bank account to a connector + operationId: v3ForwardPaymentServiceUserBankAccount + x-speakeasy-name-override: ForwardPaymentServiceUserBankAccount + parameters: + - $ref: '#/components/parameters/V3PaymentServiceUserID' + - $ref: '#/components/parameters/V3BankAccountID' + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/V3ForwardPaymentServiceUserBankAccountRequest' + responses: + "202": + description: Accepted + content: + application/json: + schema: + $ref: '#/components/schemas/V3ForwardPaymentServiceUserBankAccountResponse' + default: + description: Error + content: + application/json: + schema: + $ref: '#/components/schemas/V3ErrorResponse' + security: + - Authorization: + - payments:write /v3/pools: post: tags: @@ -4598,6 +4734,164 @@ components: - UNKNOWN - TRANSFER - PAYOUT + V3CreatePaymentServiceUserRequest: + type: object + required: + - name + properties: + name: + type: string + contactDetails: + $ref: '#/components/schemas/V3ContactDetailsRequest' + nullable: true + address: + $ref: '#/components/schemas/V3AddressRequest' + nullable: true + bankAccountIDs: + type: array + items: + type: string + nullable: true + metadata: + $ref: '#/components/schemas/V3Metadata' + V3AddressRequest: + type: object + properties: + streetNumber: + type: string + streetName: + type: string + city: + type: string + region: + type: string + postalCode: + type: string + country: + type: string + V3ContactDetailsRequest: + type: object + properties: + email: + type: string + phoneNuber: + type: string + V3CreatePaymentServiceUserResponse: + type: object + required: + - data + properties: + data: + description: The ID of the created payment service user + type: string + V3PaymentServiceUsersCursorResponse: + type: object + required: + - cursor + properties: + cursor: + type: object + required: + - pageSize + - hasMore + - data + properties: + pageSize: + type: integer + format: int64 + minimum: 1 + example: 15 + hasMore: + type: boolean + example: false + previous: + type: string + example: YXVsdCBhbmQgYSBtYXhpbXVtIG1heF9yZXN1bHRzLol= + next: + type: string + example: YXVsdCBhbmQgYSBtYXhpbXVtIG1heF9yZXN1bHRzLol= + data: + type: array + items: + $ref: '#/components/schemas/V3PaymentServiceUser' + V3PaymentServiceUser: + type: object + required: + - id + - name + - createdAt + properties: + id: + type: string + name: + type: string + createdAt: + type: string + format: date-time + contactDetails: + $ref: '#/components/schemas/V3ContactDetails' + nullable: true + address: + $ref: '#/components/schemas/V3Address' + nullable: true + bankAccountIDs: + type: array + items: + type: string + nullable: true + metadata: + $ref: '#/components/schemas/V3Metadata' + V3ContactDetails: + type: object + properties: + email: + type: string + phoneNumber: + type: string + V3Address: + type: object + properties: + streetNumber: + type: string + streetName: + type: string + city: + type: string + region: + type: string + postalCode: + type: string + country: + type: string + V3GetPaymentServiceUserResponse: + type: object + required: + - data + properties: + data: + $ref: '#/components/schemas/V3PaymentServiceUser' + V3ForwardPaymentServiceUserBankAccountRequest: + type: object + required: + - connectorID + properties: + connectorID: + type: string + format: byte + V3ForwardPaymentServiceUserBankAccountResponse: + type: object + required: + - data + properties: + data: + type: object + required: + - taskID + properties: + taskID: + description: | + Since this call is asynchronous, the response will contain the ID of the task that was created to forward the bank account to the PSP. You can use the task API to check the status of the task and get the resulting bank account ID. + type: string V3CreatePoolRequest: type: object required: @@ -5206,6 +5500,13 @@ components: description: The bank account ID schema: type: string + V3PaymentServiceUserID: + name: paymentServiceUserID + in: path + required: true + description: The payment service user ID + schema: + type: string V3PaymentID: name: paymentID in: path diff --git a/openapi/v3/v3-api.yaml b/openapi/v3/v3-api.yaml index e9773e85f..9e0965fba 100644 --- a/openapi/v3/v3-api.yaml +++ b/openapi/v3/v3-api.yaml @@ -930,6 +930,148 @@ paths: - Authorization: - payments:read + # PAYMENT SERVICE USERS + /v3/payment-service-users: + post: + tags: + - payments.v3 + summary: Create a formance payment service user object + operationId: v3CreatePaymentServiceUser + x-speakeasy-name-override: CreatePaymentServiceUser + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/V3CreatePaymentServiceUserRequest" + responses: + "201": + description: Created + content: + application/json: + schema: + $ref: "#/components/schemas/V3CreatePaymentServiceUserResponse" + default: + description: Error + content: + application/json: + schema: + $ref: "#/components/schemas/V3ErrorResponse" + security: + - Authorization: + - payments:write + get: + tags: + - payments.v3 + summary: List all payment service users + operationId: v3ListPaymentServiceUsers + x-speakeasy-name-override: ListPaymentServiceUsers + parameters: + - $ref: '#/components/parameters/V3PageSize' + - $ref: '#/components/parameters/V3Cursor' + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/V3QueryBuilder" + responses: + "200": + description: OK + content: + application/json: + schema: + $ref: "#/components/schemas/V3PaymentServiceUsersCursorResponse" + default: + description: Error + content: + application/json: + schema: + $ref: "#/components/schemas/V3ErrorResponse" + security: + - Authorization: + - payments:read + + /v3/payment-service-users/{paymentServiceUserID}: + get: + tags: + - payments.v3 + summary: Get a payment service user by ID + operationId: v3GetPaymentServiceUser + x-speakeasy-name-override: GetPaymentServiceUser + parameters: + - $ref: '#/components/parameters/V3PaymentServiceUserID' + responses: + "200": + description: OK + content: + application/json: + schema: + $ref: "#/components/schemas/V3GetPaymentServiceUserResponse" + default: + description: Error + content: + application/json: + schema: + $ref: "#/components/schemas/V3ErrorResponse" + security: + - Authorization: + - payments:read + + /v3/payment-service-users/{paymentServiceUserID}/bank-accounts/{bankAccountID}: + post: + tags: + - payments.v3 + summary: Add a bank account to a payment service user + operationId: v3AddBankAccountToPaymentServiceUser + x-speakeasy-name-override: AddBankAccountToPaymentServiceUser + parameters: + - $ref: '#/components/parameters/V3PaymentServiceUserID' + - $ref: '#/components/parameters/V3BankAccountID' + responses: + "204": + description: No Content + default: + description: Error + content: + application/json: + schema: + $ref: "#/components/schemas/V3ErrorResponse" + security: + - Authorization: + - payments:write + + /v3/payment-service-users/{paymentServiceUserID}/bank-accounts/{bankAccountID}/forward: + post: + tags: + - payments.v3 + summary: Forward a payment service user's bank account to a connector + operationId: v3ForwardPaymentServiceUserBankAccount + x-speakeasy-name-override: ForwardPaymentServiceUserBankAccount + parameters: + - $ref: '#/components/parameters/V3PaymentServiceUserID' + - $ref: '#/components/parameters/V3BankAccountID' + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/V3ForwardPaymentServiceUserBankAccountRequest" + responses: + "202": + description: Accepted + content: + application/json: + schema: + $ref: "#/components/schemas/V3ForwardPaymentServiceUserBankAccountResponse" + default: + description: Error + content: + application/json: + schema: + $ref: "#/components/schemas/V3ErrorResponse" + security: + - Authorization: + - payments:write + + # POOLS /v3/pools: post: diff --git a/openapi/v3/v3-parameters.yaml b/openapi/v3/v3-parameters.yaml index b26e55f2f..f109e1894 100644 --- a/openapi/v3/v3-parameters.yaml +++ b/openapi/v3/v3-parameters.yaml @@ -21,6 +21,14 @@ components: schema: type: string + V3PaymentServiceUserID: + name: paymentServiceUserID + in: path + required: true + description: The payment service user ID + schema: + type: string + V3PaymentID: name: paymentID in: path diff --git a/openapi/v3/v3-schemas.yaml b/openapi/v3/v3-schemas.yaml index 5d2e62dc7..de51d1823 100644 --- a/openapi/v3/v3-schemas.yaml +++ b/openapi/v3/v3-schemas.yaml @@ -1149,6 +1149,177 @@ components: - TRANSFER - PAYOUT + # PAYMENT SERVICE USERS + V3CreatePaymentServiceUserRequest: + type: object + required: + - name + properties: + name: + type: string + contactDetails: + $ref: '#/components/schemas/V3ContactDetailsRequest' + nullable: true + address: + $ref: '#/components/schemas/V3AddressRequest' + nullable: true + bankAccountIDs: + type: array + items: + type: string + nullable: true + metadata: + $ref: '#/components/schemas/V3Metadata' + + V3AddressRequest: + type: object + properties: + streetNumber: + type: string + streetName: + type: string + city: + type: string + region: + type: string + postalCode: + type: string + country: + type: string + + V3ContactDetailsRequest: + type: object + properties: + email: + type: string + phoneNuber: + type: string + + V3CreatePaymentServiceUserResponse: + type: object + required: + - data + properties: + data: + description: The ID of the created payment service user + type: string + + V3PaymentServiceUsersCursorResponse: + type: object + required: + - cursor + properties: + cursor: + type: object + required: + - pageSize + - hasMore + - data + properties: + pageSize: + type: integer + format: int64 + minimum: 1 + example: 15 + hasMore: + type: boolean + example: false + previous: + type: string + example: YXVsdCBhbmQgYSBtYXhpbXVtIG1heF9yZXN1bHRzLol= + next: + type: string + example: YXVsdCBhbmQgYSBtYXhpbXVtIG1heF9yZXN1bHRzLol= + data: + type: array + items: + $ref: '#/components/schemas/V3PaymentServiceUser' + + V3PaymentServiceUser: + type: object + required: + - id + - name + - createdAt + properties: + id: + type: string + name: + type: string + createdAt: + type: string + format: date-time + contactDetails: + $ref: '#/components/schemas/V3ContactDetails' + nullable: true + address: + $ref: '#/components/schemas/V3Address' + nullable: true + bankAccountIDs: + type: array + items: + type: string + nullable: true + metadata: + $ref: '#/components/schemas/V3Metadata' + + V3ContactDetails: + type: object + properties: + email: + type: string + phoneNumber: + type: string + + V3Address: + type: object + properties: + streetNumber: + type: string + streetName: + type: string + city: + type: string + region: + type: string + postalCode: + type: string + country: + type: string + + V3GetPaymentServiceUserResponse: + type: object + required: + - data + properties: + data: + $ref: '#/components/schemas/V3PaymentServiceUser' + + V3ForwardPaymentServiceUserBankAccountRequest: + type: object + required: + - connectorID + properties: + connectorID: + type: string + format: byte + + V3ForwardPaymentServiceUserBankAccountResponse: + type: object + required: + - data + properties: + data: + type: object + required: + - taskID + properties: + taskID: + description: > + Since this call is asynchronous, the response will contain the ID of the task that was created to forward the bank account to the PSP. You can use the task API to check the status of + the task and get the resulting bank account ID. + type: string + # POOLS V3CreatePoolRequest: type: object diff --git a/pkg/client/.speakeasy/gen.lock b/pkg/client/.speakeasy/gen.lock index d878a0743..7d39d4fa0 100644 --- a/pkg/client/.speakeasy/gen.lock +++ b/pkg/client/.speakeasy/gen.lock @@ -1,7 +1,7 @@ lockVersion: 2.0.0 id: 86668c33-fde8-4164-be98-cfc24d333a1e management: - docChecksum: 2b89b94fa4d7fdc91e9e4b71b62b7378 + docChecksum: 32653a9c28a615b4231dae33001bd5ae docVersion: v1 speakeasyVersion: 1.525.0 generationVersion: 2.562.2 @@ -107,6 +107,8 @@ generatedFiles: - /models/components/v3account.go - /models/components/v3accountscursorresponse.go - /models/components/v3accounttypeenum.go + - /models/components/v3address.go + - /models/components/v3addressrequest.go - /models/components/v3adyenconfig.go - /models/components/v3approvepaymentinitiationresponse.go - /models/components/v3atlarconfig.go @@ -124,6 +126,8 @@ generatedFiles: - /models/components/v3connectorscheduleresponse.go - /models/components/v3connectorschedulescursorresponse.go - /models/components/v3connectorscursorresponse.go + - /models/components/v3contactdetails.go + - /models/components/v3contactdetailsrequest.go - /models/components/v3createaccountrequest.go - /models/components/v3createaccountresponse.go - /models/components/v3createbankaccountrequest.go @@ -131,6 +135,8 @@ generatedFiles: - /models/components/v3createpaymentadjustmentrequest.go - /models/components/v3createpaymentrequest.go - /models/components/v3createpaymentresponse.go + - /models/components/v3createpaymentserviceuserrequest.go + - /models/components/v3createpaymentserviceuserresponse.go - /models/components/v3createpoolrequest.go - /models/components/v3createpoolresponse.go - /models/components/v3currencycloudconfig.go @@ -139,12 +145,15 @@ generatedFiles: - /models/components/v3errorsenum.go - /models/components/v3forwardbankaccountrequest.go - /models/components/v3forwardbankaccountresponse.go + - /models/components/v3forwardpaymentserviceuserbankaccountrequest.go + - /models/components/v3forwardpaymentserviceuserbankaccountresponse.go - /models/components/v3genericconfig.go - /models/components/v3getaccountresponse.go - /models/components/v3getbankaccountresponse.go - /models/components/v3getconnectorconfigresponse.go - /models/components/v3getpaymentinitiationresponse.go - /models/components/v3getpaymentresponse.go + - /models/components/v3getpaymentserviceuserresponse.go - /models/components/v3getpoolresponse.go - /models/components/v3gettaskresponse.go - /models/components/v3initiatepaymentrequest.go @@ -165,6 +174,8 @@ generatedFiles: - /models/components/v3paymentinitiationstatusenum.go - /models/components/v3paymentinitiationtypeenum.go - /models/components/v3paymentscursorresponse.go + - /models/components/v3paymentserviceuser.go + - /models/components/v3paymentserviceuserscursorresponse.go - /models/components/v3paymentstatusenum.go - /models/components/v3paymenttypeenum.go - /models/components/v3pool.go @@ -229,14 +240,17 @@ generatedFiles: - /models/operations/updatemetadata.go - /models/operations/updatetransferinitiationstatus.go - /models/operations/v3addaccounttopool.go + - /models/operations/v3addbankaccounttopaymentserviceuser.go - /models/operations/v3approvepaymentinitiation.go - /models/operations/v3createaccount.go - /models/operations/v3createbankaccount.go - /models/operations/v3createpayment.go + - /models/operations/v3createpaymentserviceuser.go - /models/operations/v3createpool.go - /models/operations/v3deletepaymentinitiation.go - /models/operations/v3deletepool.go - /models/operations/v3forwardbankaccount.go + - /models/operations/v3forwardpaymentserviceuserbankaccount.go - /models/operations/v3getaccount.go - /models/operations/v3getaccountbalances.go - /models/operations/v3getbankaccount.go @@ -244,6 +258,7 @@ generatedFiles: - /models/operations/v3getconnectorschedule.go - /models/operations/v3getpayment.go - /models/operations/v3getpaymentinitiation.go + - /models/operations/v3getpaymentserviceuser.go - /models/operations/v3getpool.go - /models/operations/v3getpoolbalances.go - /models/operations/v3gettask.go @@ -259,6 +274,7 @@ generatedFiles: - /models/operations/v3listpaymentinitiationrelatedpayments.go - /models/operations/v3listpaymentinitiations.go - /models/operations/v3listpayments.go + - /models/operations/v3listpaymentserviceusers.go - /models/operations/v3listpools.go - /models/operations/v3rejectpaymentinitiation.go - /models/operations/v3removeaccountfrompool.go @@ -383,6 +399,8 @@ generatedFiles: - docs/models/components/v3accountscursorresponse.md - docs/models/components/v3accountscursorresponsecursor.md - docs/models/components/v3accounttypeenum.md + - docs/models/components/v3address.md + - docs/models/components/v3addressrequest.md - docs/models/components/v3adyenconfig.md - docs/models/components/v3approvepaymentinitiationresponse.md - docs/models/components/v3approvepaymentinitiationresponsedata.md @@ -407,6 +425,8 @@ generatedFiles: - docs/models/components/v3connectorschedulescursorresponsecursor.md - docs/models/components/v3connectorscursorresponse.md - docs/models/components/v3connectorscursorresponsecursor.md + - docs/models/components/v3contactdetails.md + - docs/models/components/v3contactdetailsrequest.md - docs/models/components/v3createaccountrequest.md - docs/models/components/v3createaccountresponse.md - docs/models/components/v3createbankaccountrequest.md @@ -414,6 +434,8 @@ generatedFiles: - docs/models/components/v3createpaymentadjustmentrequest.md - docs/models/components/v3createpaymentrequest.md - docs/models/components/v3createpaymentresponse.md + - docs/models/components/v3createpaymentserviceuserrequest.md + - docs/models/components/v3createpaymentserviceuserresponse.md - docs/models/components/v3createpoolrequest.md - docs/models/components/v3createpoolresponse.md - docs/models/components/v3currencycloudconfig.md @@ -423,12 +445,16 @@ generatedFiles: - docs/models/components/v3forwardbankaccountrequest.md - docs/models/components/v3forwardbankaccountresponse.md - docs/models/components/v3forwardbankaccountresponsedata.md + - docs/models/components/v3forwardpaymentserviceuserbankaccountrequest.md + - docs/models/components/v3forwardpaymentserviceuserbankaccountresponse.md + - docs/models/components/v3forwardpaymentserviceuserbankaccountresponsedata.md - docs/models/components/v3genericconfig.md - docs/models/components/v3getaccountresponse.md - docs/models/components/v3getbankaccountresponse.md - docs/models/components/v3getconnectorconfigresponse.md - docs/models/components/v3getpaymentinitiationresponse.md - docs/models/components/v3getpaymentresponse.md + - docs/models/components/v3getpaymentserviceuserresponse.md - docs/models/components/v3getpoolresponse.md - docs/models/components/v3gettaskresponse.md - docs/models/components/v3initiatepaymentrequest.md @@ -455,6 +481,9 @@ generatedFiles: - docs/models/components/v3paymentinitiationtypeenum.md - docs/models/components/v3paymentscursorresponse.md - docs/models/components/v3paymentscursorresponsecursor.md + - docs/models/components/v3paymentserviceuser.md + - docs/models/components/v3paymentserviceuserscursorresponse.md + - docs/models/components/v3paymentserviceuserscursorresponsecursor.md - docs/models/components/v3paymentstatusenum.md - docs/models/components/v3paymenttypeenum.md - docs/models/components/v3pool.md @@ -560,11 +589,14 @@ generatedFiles: - docs/models/operations/updatetransferinitiationstatusresponse.md - docs/models/operations/v3addaccounttopoolrequest.md - docs/models/operations/v3addaccounttopoolresponse.md + - docs/models/operations/v3addbankaccounttopaymentserviceuserrequest.md + - docs/models/operations/v3addbankaccounttopaymentserviceuserresponse.md - docs/models/operations/v3approvepaymentinitiationrequest.md - docs/models/operations/v3approvepaymentinitiationresponse.md - docs/models/operations/v3createaccountresponse.md - docs/models/operations/v3createbankaccountresponse.md - docs/models/operations/v3createpaymentresponse.md + - docs/models/operations/v3createpaymentserviceuserresponse.md - docs/models/operations/v3createpoolresponse.md - docs/models/operations/v3deletepaymentinitiationrequest.md - docs/models/operations/v3deletepaymentinitiationresponse.md @@ -572,6 +604,8 @@ generatedFiles: - docs/models/operations/v3deletepoolresponse.md - docs/models/operations/v3forwardbankaccountrequest.md - docs/models/operations/v3forwardbankaccountresponse.md + - docs/models/operations/v3forwardpaymentserviceuserbankaccountrequest.md + - docs/models/operations/v3forwardpaymentserviceuserbankaccountresponse.md - docs/models/operations/v3getaccountbalancesrequest.md - docs/models/operations/v3getaccountbalancesresponse.md - docs/models/operations/v3getaccountrequest.md @@ -586,6 +620,8 @@ generatedFiles: - docs/models/operations/v3getpaymentinitiationresponse.md - docs/models/operations/v3getpaymentrequest.md - docs/models/operations/v3getpaymentresponse.md + - docs/models/operations/v3getpaymentserviceuserrequest.md + - docs/models/operations/v3getpaymentserviceuserresponse.md - docs/models/operations/v3getpoolbalancesrequest.md - docs/models/operations/v3getpoolbalancesresponse.md - docs/models/operations/v3getpoolrequest.md @@ -613,6 +649,8 @@ generatedFiles: - docs/models/operations/v3listpaymentinitiationrelatedpaymentsresponse.md - docs/models/operations/v3listpaymentinitiationsrequest.md - docs/models/operations/v3listpaymentinitiationsresponse.md + - docs/models/operations/v3listpaymentserviceusersrequest.md + - docs/models/operations/v3listpaymentserviceusersresponse.md - docs/models/operations/v3listpaymentsrequest.md - docs/models/operations/v3listpaymentsresponse.md - docs/models/operations/v3listpoolsrequest.md @@ -1517,5 +1555,53 @@ examples: responses: default: application/json: {"errorCode": "VALIDATION", "errorMessage": "[VALIDATION] missing reference"} + v3CreatePaymentServiceUser: + speakeasy-default-v3-create-payment-service-user: + responses: + "201": + application/json: {"data": ""} + default: + application/json: {"errorCode": "VALIDATION", "errorMessage": "[VALIDATION] missing required config field: pollingPeriod"} + v3ListPaymentServiceUsers: + "": + parameters: + query: + pageSize: 100 + cursor: "aHR0cHM6Ly9nLnBhZ2UvTmVrby1SYW1lbj9zaGFyZQ==" + responses: + "200": + application/json: {"cursor": {"pageSize": 15, "hasMore": false, "previous": "YXVsdCBhbmQgYSBtYXhpbXVtIG1heF9yZXN1bHRzLol=", "next": "YXVsdCBhbmQgYSBtYXhpbXVtIG1heF9yZXN1bHRzLol=", "data": []}} + default: + application/json: {"errorCode": "VALIDATION", "errorMessage": "[VALIDATION] missing required config field: pollingPeriod"} + v3GetPaymentServiceUser: + speakeasy-default-v3-get-payment-service-user: + parameters: + path: + paymentServiceUserID: "" + responses: + "200": + application/json: {"data": {"id": "", "name": "", "createdAt": "2024-06-15T05:02:42.695Z"}} + default: + application/json: {"errorCode": "VALIDATION", "errorMessage": "[VALIDATION] missing required config field: pollingPeriod"} + v3AddBankAccountToPaymentServiceUser: + speakeasy-default-v3-add-bank-account-to-payment-service-user: + parameters: + path: + paymentServiceUserID: "" + bankAccountID: "" + responses: + default: + application/json: {"errorCode": "VALIDATION", "errorMessage": "[VALIDATION] missing required config field: pollingPeriod"} + v3ForwardPaymentServiceUserBankAccount: + speakeasy-default-v3-forward-payment-service-user-bank-account: + parameters: + path: + paymentServiceUserID: "" + bankAccountID: "" + responses: + "202": + application/json: {"data": {"taskID": ""}} + default: + application/json: {"errorCode": "VALIDATION", "errorMessage": "[VALIDATION] missing required config field: pollingPeriod"} examplesVersion: 1.0.0 generatedTests: {} diff --git a/pkg/client/README.md b/pkg/client/README.md index a46e1e277..36cefc6f5 100644 --- a/pkg/client/README.md +++ b/pkg/client/README.md @@ -217,6 +217,11 @@ func main() { * [ReversePaymentInitiation](docs/sdks/v3/README.md#reversepaymentinitiation) - Reverse a payment initiation * [ListPaymentInitiationAdjustments](docs/sdks/v3/README.md#listpaymentinitiationadjustments) - List all payment initiation adjustments * [ListPaymentInitiationRelatedPayments](docs/sdks/v3/README.md#listpaymentinitiationrelatedpayments) - List all payments related to a payment initiation +* [CreatePaymentServiceUser](docs/sdks/v3/README.md#createpaymentserviceuser) - Create a formance payment service user object +* [ListPaymentServiceUsers](docs/sdks/v3/README.md#listpaymentserviceusers) - List all payment service users +* [GetPaymentServiceUser](docs/sdks/v3/README.md#getpaymentserviceuser) - Get a payment service user by ID +* [AddBankAccountToPaymentServiceUser](docs/sdks/v3/README.md#addbankaccounttopaymentserviceuser) - Add a bank account to a payment service user +* [ForwardPaymentServiceUserBankAccount](docs/sdks/v3/README.md#forwardpaymentserviceuserbankaccount) - Forward a payment service user's bank account to a connector * [CreatePool](docs/sdks/v3/README.md#createpool) - Create a formance pool object * [ListPools](docs/sdks/v3/README.md#listpools) - List all pools * [GetPool](docs/sdks/v3/README.md#getpool) - Get a pool by ID diff --git a/pkg/client/docs/models/components/v3address.md b/pkg/client/docs/models/components/v3address.md new file mode 100644 index 000000000..04e7d81b0 --- /dev/null +++ b/pkg/client/docs/models/components/v3address.md @@ -0,0 +1,13 @@ +# V3Address + + +## Fields + +| Field | Type | Required | Description | +| ------------------ | ------------------ | ------------------ | ------------------ | +| `StreetNumber` | **string* | :heavy_minus_sign: | N/A | +| `StreetName` | **string* | :heavy_minus_sign: | N/A | +| `City` | **string* | :heavy_minus_sign: | N/A | +| `Region` | **string* | :heavy_minus_sign: | N/A | +| `PostalCode` | **string* | :heavy_minus_sign: | N/A | +| `Country` | **string* | :heavy_minus_sign: | N/A | \ No newline at end of file diff --git a/pkg/client/docs/models/components/v3addressrequest.md b/pkg/client/docs/models/components/v3addressrequest.md new file mode 100644 index 000000000..64e0ee252 --- /dev/null +++ b/pkg/client/docs/models/components/v3addressrequest.md @@ -0,0 +1,13 @@ +# V3AddressRequest + + +## Fields + +| Field | Type | Required | Description | +| ------------------ | ------------------ | ------------------ | ------------------ | +| `StreetNumber` | **string* | :heavy_minus_sign: | N/A | +| `StreetName` | **string* | :heavy_minus_sign: | N/A | +| `City` | **string* | :heavy_minus_sign: | N/A | +| `Region` | **string* | :heavy_minus_sign: | N/A | +| `PostalCode` | **string* | :heavy_minus_sign: | N/A | +| `Country` | **string* | :heavy_minus_sign: | N/A | \ No newline at end of file diff --git a/pkg/client/docs/models/components/v3contactdetails.md b/pkg/client/docs/models/components/v3contactdetails.md new file mode 100644 index 000000000..cda7bacf8 --- /dev/null +++ b/pkg/client/docs/models/components/v3contactdetails.md @@ -0,0 +1,9 @@ +# V3ContactDetails + + +## Fields + +| Field | Type | Required | Description | +| ------------------ | ------------------ | ------------------ | ------------------ | +| `Email` | **string* | :heavy_minus_sign: | N/A | +| `PhoneNumber` | **string* | :heavy_minus_sign: | N/A | \ No newline at end of file diff --git a/pkg/client/docs/models/components/v3contactdetailsrequest.md b/pkg/client/docs/models/components/v3contactdetailsrequest.md new file mode 100644 index 000000000..7d4a32b81 --- /dev/null +++ b/pkg/client/docs/models/components/v3contactdetailsrequest.md @@ -0,0 +1,9 @@ +# V3ContactDetailsRequest + + +## Fields + +| Field | Type | Required | Description | +| ------------------ | ------------------ | ------------------ | ------------------ | +| `Email` | **string* | :heavy_minus_sign: | N/A | +| `PhoneNuber` | **string* | :heavy_minus_sign: | N/A | \ No newline at end of file diff --git a/pkg/client/docs/models/components/v3createpaymentserviceuserrequest.md b/pkg/client/docs/models/components/v3createpaymentserviceuserrequest.md new file mode 100644 index 000000000..bf35ad856 --- /dev/null +++ b/pkg/client/docs/models/components/v3createpaymentserviceuserrequest.md @@ -0,0 +1,12 @@ +# V3CreatePaymentServiceUserRequest + + +## Fields + +| Field | Type | Required | Description | +| ----------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------- | +| `Name` | *string* | :heavy_check_mark: | N/A | +| `ContactDetails` | [*components.V3ContactDetailsRequest](../../models/components/v3contactdetailsrequest.md) | :heavy_minus_sign: | N/A | +| `Address` | [*components.V3AddressRequest](../../models/components/v3addressrequest.md) | :heavy_minus_sign: | N/A | +| `BankAccountIDs` | []*string* | :heavy_minus_sign: | N/A | +| `Metadata` | map[string]*string* | :heavy_minus_sign: | N/A | \ No newline at end of file diff --git a/pkg/client/docs/models/components/v3createpaymentserviceuserresponse.md b/pkg/client/docs/models/components/v3createpaymentserviceuserresponse.md new file mode 100644 index 000000000..962b248e9 --- /dev/null +++ b/pkg/client/docs/models/components/v3createpaymentserviceuserresponse.md @@ -0,0 +1,8 @@ +# V3CreatePaymentServiceUserResponse + + +## Fields + +| Field | Type | Required | Description | +| ------------------------------------------ | ------------------------------------------ | ------------------------------------------ | ------------------------------------------ | +| `Data` | *string* | :heavy_check_mark: | The ID of the created payment service user | \ No newline at end of file diff --git a/pkg/client/docs/models/components/v3forwardpaymentserviceuserbankaccountrequest.md b/pkg/client/docs/models/components/v3forwardpaymentserviceuserbankaccountrequest.md new file mode 100644 index 000000000..67df6d1f6 --- /dev/null +++ b/pkg/client/docs/models/components/v3forwardpaymentserviceuserbankaccountrequest.md @@ -0,0 +1,8 @@ +# V3ForwardPaymentServiceUserBankAccountRequest + + +## Fields + +| Field | Type | Required | Description | +| ------------------ | ------------------ | ------------------ | ------------------ | +| `ConnectorID` | *string* | :heavy_check_mark: | N/A | \ No newline at end of file diff --git a/pkg/client/docs/models/components/v3forwardpaymentserviceuserbankaccountresponse.md b/pkg/client/docs/models/components/v3forwardpaymentserviceuserbankaccountresponse.md new file mode 100644 index 000000000..76b00cab9 --- /dev/null +++ b/pkg/client/docs/models/components/v3forwardpaymentserviceuserbankaccountresponse.md @@ -0,0 +1,8 @@ +# V3ForwardPaymentServiceUserBankAccountResponse + + +## Fields + +| Field | Type | Required | Description | +| ---------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- | +| `Data` | [components.V3ForwardPaymentServiceUserBankAccountResponseData](../../models/components/v3forwardpaymentserviceuserbankaccountresponsedata.md) | :heavy_check_mark: | N/A | \ No newline at end of file diff --git a/pkg/client/docs/models/components/v3forwardpaymentserviceuserbankaccountresponsedata.md b/pkg/client/docs/models/components/v3forwardpaymentserviceuserbankaccountresponsedata.md new file mode 100644 index 000000000..a60011fec --- /dev/null +++ b/pkg/client/docs/models/components/v3forwardpaymentserviceuserbankaccountresponsedata.md @@ -0,0 +1,8 @@ +# V3ForwardPaymentServiceUserBankAccountResponseData + + +## Fields + +| Field | Type | Required | Description | +| --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `TaskID` | *string* | :heavy_check_mark: | Since this call is asynchronous, the response will contain the ID of the task that was created to forward the bank account to the PSP. You can use the task API to check the status of the task and get the resulting bank account ID.
| \ No newline at end of file diff --git a/pkg/client/docs/models/components/v3getpaymentserviceuserresponse.md b/pkg/client/docs/models/components/v3getpaymentserviceuserresponse.md new file mode 100644 index 000000000..d291b1e1a --- /dev/null +++ b/pkg/client/docs/models/components/v3getpaymentserviceuserresponse.md @@ -0,0 +1,8 @@ +# V3GetPaymentServiceUserResponse + + +## Fields + +| Field | Type | Required | Description | +| ---------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------- | +| `Data` | [components.V3PaymentServiceUser](../../models/components/v3paymentserviceuser.md) | :heavy_check_mark: | N/A | \ No newline at end of file diff --git a/pkg/client/docs/models/components/v3paymentserviceuser.md b/pkg/client/docs/models/components/v3paymentserviceuser.md new file mode 100644 index 000000000..960463ec5 --- /dev/null +++ b/pkg/client/docs/models/components/v3paymentserviceuser.md @@ -0,0 +1,14 @@ +# V3PaymentServiceUser + + +## Fields + +| Field | Type | Required | Description | +| --------------------------------------------------------------------------- | --------------------------------------------------------------------------- | --------------------------------------------------------------------------- | --------------------------------------------------------------------------- | +| `ID` | *string* | :heavy_check_mark: | N/A | +| `Name` | *string* | :heavy_check_mark: | N/A | +| `CreatedAt` | [time.Time](https://pkg.go.dev/time#Time) | :heavy_check_mark: | N/A | +| `ContactDetails` | [*components.V3ContactDetails](../../models/components/v3contactdetails.md) | :heavy_minus_sign: | N/A | +| `Address` | [*components.V3Address](../../models/components/v3address.md) | :heavy_minus_sign: | N/A | +| `BankAccountIDs` | []*string* | :heavy_minus_sign: | N/A | +| `Metadata` | map[string]*string* | :heavy_minus_sign: | N/A | \ No newline at end of file diff --git a/pkg/client/docs/models/components/v3paymentserviceuserscursorresponse.md b/pkg/client/docs/models/components/v3paymentserviceuserscursorresponse.md new file mode 100644 index 000000000..c6b7f73cb --- /dev/null +++ b/pkg/client/docs/models/components/v3paymentserviceuserscursorresponse.md @@ -0,0 +1,8 @@ +# V3PaymentServiceUsersCursorResponse + + +## Fields + +| Field | Type | Required | Description | +| ---------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------- | +| `Cursor` | [components.V3PaymentServiceUsersCursorResponseCursor](../../models/components/v3paymentserviceuserscursorresponsecursor.md) | :heavy_check_mark: | N/A | \ No newline at end of file diff --git a/pkg/client/docs/models/components/v3paymentserviceuserscursorresponsecursor.md b/pkg/client/docs/models/components/v3paymentserviceuserscursorresponsecursor.md new file mode 100644 index 000000000..b3e67d3d6 --- /dev/null +++ b/pkg/client/docs/models/components/v3paymentserviceuserscursorresponsecursor.md @@ -0,0 +1,12 @@ +# V3PaymentServiceUsersCursorResponseCursor + + +## Fields + +| Field | Type | Required | Description | Example | +| ------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------ | +| `PageSize` | *int64* | :heavy_check_mark: | N/A | 15 | +| `HasMore` | *bool* | :heavy_check_mark: | N/A | false | +| `Previous` | **string* | :heavy_minus_sign: | N/A | YXVsdCBhbmQgYSBtYXhpbXVtIG1heF9yZXN1bHRzLol= | +| `Next` | **string* | :heavy_minus_sign: | N/A | YXVsdCBhbmQgYSBtYXhpbXVtIG1heF9yZXN1bHRzLol= | +| `Data` | [][components.V3PaymentServiceUser](../../models/components/v3paymentserviceuser.md) | :heavy_check_mark: | N/A | | \ No newline at end of file diff --git a/pkg/client/docs/models/operations/v3addbankaccounttopaymentserviceuserrequest.md b/pkg/client/docs/models/operations/v3addbankaccounttopaymentserviceuserrequest.md new file mode 100644 index 000000000..98bde519f --- /dev/null +++ b/pkg/client/docs/models/operations/v3addbankaccounttopaymentserviceuserrequest.md @@ -0,0 +1,9 @@ +# V3AddBankAccountToPaymentServiceUserRequest + + +## Fields + +| Field | Type | Required | Description | +| --------------------------- | --------------------------- | --------------------------- | --------------------------- | +| `PaymentServiceUserID` | *string* | :heavy_check_mark: | The payment service user ID | +| `BankAccountID` | *string* | :heavy_check_mark: | The bank account ID | \ No newline at end of file diff --git a/pkg/client/docs/models/operations/v3addbankaccounttopaymentserviceuserresponse.md b/pkg/client/docs/models/operations/v3addbankaccounttopaymentserviceuserresponse.md new file mode 100644 index 000000000..06dd2ec72 --- /dev/null +++ b/pkg/client/docs/models/operations/v3addbankaccounttopaymentserviceuserresponse.md @@ -0,0 +1,9 @@ +# V3AddBankAccountToPaymentServiceUserResponse + + +## Fields + +| Field | Type | Required | Description | +| ------------------------------------------------------------------------- | ------------------------------------------------------------------------- | ------------------------------------------------------------------------- | ------------------------------------------------------------------------- | +| `HTTPMeta` | [components.HTTPMetadata](../../models/components/httpmetadata.md) | :heavy_check_mark: | N/A | +| `V3ErrorResponse` | [*components.V3ErrorResponse](../../models/components/v3errorresponse.md) | :heavy_minus_sign: | Error | \ No newline at end of file diff --git a/pkg/client/docs/models/operations/v3createpaymentserviceuserresponse.md b/pkg/client/docs/models/operations/v3createpaymentserviceuserresponse.md new file mode 100644 index 000000000..26ece4238 --- /dev/null +++ b/pkg/client/docs/models/operations/v3createpaymentserviceuserresponse.md @@ -0,0 +1,10 @@ +# V3CreatePaymentServiceUserResponse + + +## Fields + +| Field | Type | Required | Description | +| --------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------- | +| `HTTPMeta` | [components.HTTPMetadata](../../models/components/httpmetadata.md) | :heavy_check_mark: | N/A | +| `V3CreatePaymentServiceUserResponse` | [*components.V3CreatePaymentServiceUserResponse](../../models/components/v3createpaymentserviceuserresponse.md) | :heavy_minus_sign: | Created | +| `V3ErrorResponse` | [*components.V3ErrorResponse](../../models/components/v3errorresponse.md) | :heavy_minus_sign: | Error | \ No newline at end of file diff --git a/pkg/client/docs/models/operations/v3forwardpaymentserviceuserbankaccountrequest.md b/pkg/client/docs/models/operations/v3forwardpaymentserviceuserbankaccountrequest.md new file mode 100644 index 000000000..331ac317e --- /dev/null +++ b/pkg/client/docs/models/operations/v3forwardpaymentserviceuserbankaccountrequest.md @@ -0,0 +1,10 @@ +# V3ForwardPaymentServiceUserBankAccountRequest + + +## Fields + +| Field | Type | Required | Description | +| ------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- | +| `PaymentServiceUserID` | *string* | :heavy_check_mark: | The payment service user ID | +| `BankAccountID` | *string* | :heavy_check_mark: | The bank account ID | +| `V3ForwardPaymentServiceUserBankAccountRequest` | [*components.V3ForwardPaymentServiceUserBankAccountRequest](../../models/components/v3forwardpaymentserviceuserbankaccountrequest.md) | :heavy_minus_sign: | N/A | \ No newline at end of file diff --git a/pkg/client/docs/models/operations/v3forwardpaymentserviceuserbankaccountresponse.md b/pkg/client/docs/models/operations/v3forwardpaymentserviceuserbankaccountresponse.md new file mode 100644 index 000000000..025781ce4 --- /dev/null +++ b/pkg/client/docs/models/operations/v3forwardpaymentserviceuserbankaccountresponse.md @@ -0,0 +1,10 @@ +# V3ForwardPaymentServiceUserBankAccountResponse + + +## Fields + +| Field | Type | Required | Description | +| --------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------- | +| `HTTPMeta` | [components.HTTPMetadata](../../models/components/httpmetadata.md) | :heavy_check_mark: | N/A | +| `V3ForwardPaymentServiceUserBankAccountResponse` | [*components.V3ForwardPaymentServiceUserBankAccountResponse](../../models/components/v3forwardpaymentserviceuserbankaccountresponse.md) | :heavy_minus_sign: | Accepted | +| `V3ErrorResponse` | [*components.V3ErrorResponse](../../models/components/v3errorresponse.md) | :heavy_minus_sign: | Error | \ No newline at end of file diff --git a/pkg/client/docs/models/operations/v3getpaymentserviceuserrequest.md b/pkg/client/docs/models/operations/v3getpaymentserviceuserrequest.md new file mode 100644 index 000000000..d03fb8cc3 --- /dev/null +++ b/pkg/client/docs/models/operations/v3getpaymentserviceuserrequest.md @@ -0,0 +1,8 @@ +# V3GetPaymentServiceUserRequest + + +## Fields + +| Field | Type | Required | Description | +| --------------------------- | --------------------------- | --------------------------- | --------------------------- | +| `PaymentServiceUserID` | *string* | :heavy_check_mark: | The payment service user ID | \ No newline at end of file diff --git a/pkg/client/docs/models/operations/v3getpaymentserviceuserresponse.md b/pkg/client/docs/models/operations/v3getpaymentserviceuserresponse.md new file mode 100644 index 000000000..ccfc82b10 --- /dev/null +++ b/pkg/client/docs/models/operations/v3getpaymentserviceuserresponse.md @@ -0,0 +1,10 @@ +# V3GetPaymentServiceUserResponse + + +## Fields + +| Field | Type | Required | Description | +| --------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------- | +| `HTTPMeta` | [components.HTTPMetadata](../../models/components/httpmetadata.md) | :heavy_check_mark: | N/A | +| `V3GetPaymentServiceUserResponse` | [*components.V3GetPaymentServiceUserResponse](../../models/components/v3getpaymentserviceuserresponse.md) | :heavy_minus_sign: | OK | +| `V3ErrorResponse` | [*components.V3ErrorResponse](../../models/components/v3errorresponse.md) | :heavy_minus_sign: | Error | \ No newline at end of file diff --git a/pkg/client/docs/models/operations/v3listpaymentserviceusersrequest.md b/pkg/client/docs/models/operations/v3listpaymentserviceusersrequest.md new file mode 100644 index 000000000..639bf9f1d --- /dev/null +++ b/pkg/client/docs/models/operations/v3listpaymentserviceusersrequest.md @@ -0,0 +1,10 @@ +# V3ListPaymentServiceUsersRequest + + +## Fields + +| Field | Type | Required | Description | Example | +| ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| `PageSize` | **int64* | :heavy_minus_sign: | The number of items to return | 100 | +| `Cursor` | **string* | :heavy_minus_sign: | Parameter used in pagination requests. Set to the value of next for the next page of results. Set to the value of previous for the previous page of results. No other parameters can be set when this parameter is set.
| aHR0cHM6Ly9nLnBhZ2UvTmVrby1SYW1lbj9zaGFyZQ== | +| `RequestBody` | map[string]*any* | :heavy_minus_sign: | N/A | | \ No newline at end of file diff --git a/pkg/client/docs/models/operations/v3listpaymentserviceusersresponse.md b/pkg/client/docs/models/operations/v3listpaymentserviceusersresponse.md new file mode 100644 index 000000000..8dc7a4696 --- /dev/null +++ b/pkg/client/docs/models/operations/v3listpaymentserviceusersresponse.md @@ -0,0 +1,10 @@ +# V3ListPaymentServiceUsersResponse + + +## Fields + +| Field | Type | Required | Description | +| ----------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------- | +| `HTTPMeta` | [components.HTTPMetadata](../../models/components/httpmetadata.md) | :heavy_check_mark: | N/A | +| `V3PaymentServiceUsersCursorResponse` | [*components.V3PaymentServiceUsersCursorResponse](../../models/components/v3paymentserviceuserscursorresponse.md) | :heavy_minus_sign: | OK | +| `V3ErrorResponse` | [*components.V3ErrorResponse](../../models/components/v3errorresponse.md) | :heavy_minus_sign: | Error | \ No newline at end of file diff --git a/pkg/client/docs/sdks/v3/README.md b/pkg/client/docs/sdks/v3/README.md index 1ea95759b..5a9b4f16b 100644 --- a/pkg/client/docs/sdks/v3/README.md +++ b/pkg/client/docs/sdks/v3/README.md @@ -41,6 +41,11 @@ * [ReversePaymentInitiation](#reversepaymentinitiation) - Reverse a payment initiation * [ListPaymentInitiationAdjustments](#listpaymentinitiationadjustments) - List all payment initiation adjustments * [ListPaymentInitiationRelatedPayments](#listpaymentinitiationrelatedpayments) - List all payments related to a payment initiation +* [CreatePaymentServiceUser](#createpaymentserviceuser) - Create a formance payment service user object +* [ListPaymentServiceUsers](#listpaymentserviceusers) - List all payment service users +* [GetPaymentServiceUser](#getpaymentserviceuser) - Get a payment service user by ID +* [AddBankAccountToPaymentServiceUser](#addbankaccounttopaymentserviceuser) - Add a bank account to a payment service user +* [ForwardPaymentServiceUserBankAccount](#forwardpaymentserviceuserbankaccount) - Forward a payment service user's bank account to a connector * [CreatePool](#createpool) - Create a formance pool object * [ListPools](#listpools) - List all pools * [GetPool](#getpool) - Get a pool by ID @@ -1803,6 +1808,271 @@ func main() { | ------------------ | ------------------ | ------------------ | | sdkerrors.SDKError | 4XX, 5XX | \*/\* | +## CreatePaymentServiceUser + +Create a formance payment service user object + +### Example Usage + +```go +package main + +import( + "context" + "github.com/formancehq/payments/pkg/client" + "os" + "log" +) + +func main() { + ctx := context.Background() + + s := client.New( + "https://api.example.com", + client.WithSecurity(os.Getenv("FORMANCE_AUTHORIZATION")), + ) + + res, err := s.Payments.V3.CreatePaymentServiceUser(ctx, nil) + if err != nil { + log.Fatal(err) + } + if res.V3CreatePaymentServiceUserResponse != nil { + // handle response + } +} +``` + +### Parameters + +| Parameter | Type | Required | Description | +| ------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------ | +| `ctx` | [context.Context](https://pkg.go.dev/context#Context) | :heavy_check_mark: | The context to use for the request. | +| `request` | [components.V3CreatePaymentServiceUserRequest](../../models/components/v3createpaymentserviceuserrequest.md) | :heavy_check_mark: | The request object to use for the request. | +| `opts` | [][operations.Option](../../models/operations/option.md) | :heavy_minus_sign: | The options for this request. | + +### Response + +**[*operations.V3CreatePaymentServiceUserResponse](../../models/operations/v3createpaymentserviceuserresponse.md), error** + +### Errors + +| Error Type | Status Code | Content Type | +| ------------------ | ------------------ | ------------------ | +| sdkerrors.SDKError | 4XX, 5XX | \*/\* | + +## ListPaymentServiceUsers + +List all payment service users + +### Example Usage + +```go +package main + +import( + "context" + "github.com/formancehq/payments/pkg/client" + "os" + "log" +) + +func main() { + ctx := context.Background() + + s := client.New( + "https://api.example.com", + client.WithSecurity(os.Getenv("FORMANCE_AUTHORIZATION")), + ) + + res, err := s.Payments.V3.ListPaymentServiceUsers(ctx, client.Int64(100), client.String("aHR0cHM6Ly9nLnBhZ2UvTmVrby1SYW1lbj9zaGFyZQ=="), nil) + if err != nil { + log.Fatal(err) + } + if res.V3PaymentServiceUsersCursorResponse != nil { + // handle response + } +} +``` + +### Parameters + +| Parameter | Type | Required | Description | Example | +| ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| `ctx` | [context.Context](https://pkg.go.dev/context#Context) | :heavy_check_mark: | The context to use for the request. | | +| `pageSize` | **int64* | :heavy_minus_sign: | The number of items to return | 100 | +| `cursor` | **string* | :heavy_minus_sign: | Parameter used in pagination requests. Set to the value of next for the next page of results. Set to the value of previous for the previous page of results. No other parameters can be set when this parameter is set.
| aHR0cHM6Ly9nLnBhZ2UvTmVrby1SYW1lbj9zaGFyZQ== | +| `requestBody` | map[string]*any* | :heavy_minus_sign: | N/A | | +| `opts` | [][operations.Option](../../models/operations/option.md) | :heavy_minus_sign: | The options for this request. | | + +### Response + +**[*operations.V3ListPaymentServiceUsersResponse](../../models/operations/v3listpaymentserviceusersresponse.md), error** + +### Errors + +| Error Type | Status Code | Content Type | +| ------------------ | ------------------ | ------------------ | +| sdkerrors.SDKError | 4XX, 5XX | \*/\* | + +## GetPaymentServiceUser + +Get a payment service user by ID + +### Example Usage + +```go +package main + +import( + "context" + "github.com/formancehq/payments/pkg/client" + "os" + "log" +) + +func main() { + ctx := context.Background() + + s := client.New( + "https://api.example.com", + client.WithSecurity(os.Getenv("FORMANCE_AUTHORIZATION")), + ) + + res, err := s.Payments.V3.GetPaymentServiceUser(ctx, "") + if err != nil { + log.Fatal(err) + } + if res.V3GetPaymentServiceUserResponse != nil { + // handle response + } +} +``` + +### Parameters + +| Parameter | Type | Required | Description | +| -------------------------------------------------------- | -------------------------------------------------------- | -------------------------------------------------------- | -------------------------------------------------------- | +| `ctx` | [context.Context](https://pkg.go.dev/context#Context) | :heavy_check_mark: | The context to use for the request. | +| `paymentServiceUserID` | *string* | :heavy_check_mark: | The payment service user ID | +| `opts` | [][operations.Option](../../models/operations/option.md) | :heavy_minus_sign: | The options for this request. | + +### Response + +**[*operations.V3GetPaymentServiceUserResponse](../../models/operations/v3getpaymentserviceuserresponse.md), error** + +### Errors + +| Error Type | Status Code | Content Type | +| ------------------ | ------------------ | ------------------ | +| sdkerrors.SDKError | 4XX, 5XX | \*/\* | + +## AddBankAccountToPaymentServiceUser + +Add a bank account to a payment service user + +### Example Usage + +```go +package main + +import( + "context" + "github.com/formancehq/payments/pkg/client" + "os" + "log" +) + +func main() { + ctx := context.Background() + + s := client.New( + "https://api.example.com", + client.WithSecurity(os.Getenv("FORMANCE_AUTHORIZATION")), + ) + + res, err := s.Payments.V3.AddBankAccountToPaymentServiceUser(ctx, "", "") + if err != nil { + log.Fatal(err) + } + if res != nil { + // handle response + } +} +``` + +### Parameters + +| Parameter | Type | Required | Description | +| -------------------------------------------------------- | -------------------------------------------------------- | -------------------------------------------------------- | -------------------------------------------------------- | +| `ctx` | [context.Context](https://pkg.go.dev/context#Context) | :heavy_check_mark: | The context to use for the request. | +| `paymentServiceUserID` | *string* | :heavy_check_mark: | The payment service user ID | +| `bankAccountID` | *string* | :heavy_check_mark: | The bank account ID | +| `opts` | [][operations.Option](../../models/operations/option.md) | :heavy_minus_sign: | The options for this request. | + +### Response + +**[*operations.V3AddBankAccountToPaymentServiceUserResponse](../../models/operations/v3addbankaccounttopaymentserviceuserresponse.md), error** + +### Errors + +| Error Type | Status Code | Content Type | +| ------------------ | ------------------ | ------------------ | +| sdkerrors.SDKError | 4XX, 5XX | \*/\* | + +## ForwardPaymentServiceUserBankAccount + +Forward a payment service user's bank account to a connector + +### Example Usage + +```go +package main + +import( + "context" + "github.com/formancehq/payments/pkg/client" + "os" + "log" +) + +func main() { + ctx := context.Background() + + s := client.New( + "https://api.example.com", + client.WithSecurity(os.Getenv("FORMANCE_AUTHORIZATION")), + ) + + res, err := s.Payments.V3.ForwardPaymentServiceUserBankAccount(ctx, "", "", nil) + if err != nil { + log.Fatal(err) + } + if res.V3ForwardPaymentServiceUserBankAccountResponse != nil { + // handle response + } +} +``` + +### Parameters + +| Parameter | Type | Required | Description | +| ------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- | +| `ctx` | [context.Context](https://pkg.go.dev/context#Context) | :heavy_check_mark: | The context to use for the request. | +| `paymentServiceUserID` | *string* | :heavy_check_mark: | The payment service user ID | +| `bankAccountID` | *string* | :heavy_check_mark: | The bank account ID | +| `v3ForwardPaymentServiceUserBankAccountRequest` | [*components.V3ForwardPaymentServiceUserBankAccountRequest](../../models/components/v3forwardpaymentserviceuserbankaccountrequest.md) | :heavy_minus_sign: | N/A | +| `opts` | [][operations.Option](../../models/operations/option.md) | :heavy_minus_sign: | The options for this request. | + +### Response + +**[*operations.V3ForwardPaymentServiceUserBankAccountResponse](../../models/operations/v3forwardpaymentserviceuserbankaccountresponse.md), error** + +### Errors + +| Error Type | Status Code | Content Type | +| ------------------ | ------------------ | ------------------ | +| sdkerrors.SDKError | 4XX, 5XX | \*/\* | + ## CreatePool Create a formance pool object diff --git a/pkg/client/models/components/v3address.go b/pkg/client/models/components/v3address.go new file mode 100644 index 000000000..74e903a41 --- /dev/null +++ b/pkg/client/models/components/v3address.go @@ -0,0 +1,54 @@ +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package components + +type V3Address struct { + StreetNumber *string `json:"streetNumber,omitempty"` + StreetName *string `json:"streetName,omitempty"` + City *string `json:"city,omitempty"` + Region *string `json:"region,omitempty"` + PostalCode *string `json:"postalCode,omitempty"` + Country *string `json:"country,omitempty"` +} + +func (o *V3Address) GetStreetNumber() *string { + if o == nil { + return nil + } + return o.StreetNumber +} + +func (o *V3Address) GetStreetName() *string { + if o == nil { + return nil + } + return o.StreetName +} + +func (o *V3Address) GetCity() *string { + if o == nil { + return nil + } + return o.City +} + +func (o *V3Address) GetRegion() *string { + if o == nil { + return nil + } + return o.Region +} + +func (o *V3Address) GetPostalCode() *string { + if o == nil { + return nil + } + return o.PostalCode +} + +func (o *V3Address) GetCountry() *string { + if o == nil { + return nil + } + return o.Country +} diff --git a/pkg/client/models/components/v3addressrequest.go b/pkg/client/models/components/v3addressrequest.go new file mode 100644 index 000000000..02cd280b8 --- /dev/null +++ b/pkg/client/models/components/v3addressrequest.go @@ -0,0 +1,54 @@ +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package components + +type V3AddressRequest struct { + StreetNumber *string `json:"streetNumber,omitempty"` + StreetName *string `json:"streetName,omitempty"` + City *string `json:"city,omitempty"` + Region *string `json:"region,omitempty"` + PostalCode *string `json:"postalCode,omitempty"` + Country *string `json:"country,omitempty"` +} + +func (o *V3AddressRequest) GetStreetNumber() *string { + if o == nil { + return nil + } + return o.StreetNumber +} + +func (o *V3AddressRequest) GetStreetName() *string { + if o == nil { + return nil + } + return o.StreetName +} + +func (o *V3AddressRequest) GetCity() *string { + if o == nil { + return nil + } + return o.City +} + +func (o *V3AddressRequest) GetRegion() *string { + if o == nil { + return nil + } + return o.Region +} + +func (o *V3AddressRequest) GetPostalCode() *string { + if o == nil { + return nil + } + return o.PostalCode +} + +func (o *V3AddressRequest) GetCountry() *string { + if o == nil { + return nil + } + return o.Country +} diff --git a/pkg/client/models/components/v3contactdetails.go b/pkg/client/models/components/v3contactdetails.go new file mode 100644 index 000000000..12234c798 --- /dev/null +++ b/pkg/client/models/components/v3contactdetails.go @@ -0,0 +1,22 @@ +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package components + +type V3ContactDetails struct { + Email *string `json:"email,omitempty"` + PhoneNumber *string `json:"phoneNumber,omitempty"` +} + +func (o *V3ContactDetails) GetEmail() *string { + if o == nil { + return nil + } + return o.Email +} + +func (o *V3ContactDetails) GetPhoneNumber() *string { + if o == nil { + return nil + } + return o.PhoneNumber +} diff --git a/pkg/client/models/components/v3contactdetailsrequest.go b/pkg/client/models/components/v3contactdetailsrequest.go new file mode 100644 index 000000000..1b2c8c685 --- /dev/null +++ b/pkg/client/models/components/v3contactdetailsrequest.go @@ -0,0 +1,22 @@ +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package components + +type V3ContactDetailsRequest struct { + Email *string `json:"email,omitempty"` + PhoneNuber *string `json:"phoneNuber,omitempty"` +} + +func (o *V3ContactDetailsRequest) GetEmail() *string { + if o == nil { + return nil + } + return o.Email +} + +func (o *V3ContactDetailsRequest) GetPhoneNuber() *string { + if o == nil { + return nil + } + return o.PhoneNuber +} diff --git a/pkg/client/models/components/v3createpaymentserviceuserrequest.go b/pkg/client/models/components/v3createpaymentserviceuserrequest.go new file mode 100644 index 000000000..be8cc8870 --- /dev/null +++ b/pkg/client/models/components/v3createpaymentserviceuserrequest.go @@ -0,0 +1,46 @@ +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package components + +type V3CreatePaymentServiceUserRequest struct { + Name string `json:"name"` + ContactDetails *V3ContactDetailsRequest `json:"contactDetails,omitempty"` + Address *V3AddressRequest `json:"address,omitempty"` + BankAccountIDs []string `json:"bankAccountIDs,omitempty"` + Metadata map[string]string `json:"metadata,omitempty"` +} + +func (o *V3CreatePaymentServiceUserRequest) GetName() string { + if o == nil { + return "" + } + return o.Name +} + +func (o *V3CreatePaymentServiceUserRequest) GetContactDetails() *V3ContactDetailsRequest { + if o == nil { + return nil + } + return o.ContactDetails +} + +func (o *V3CreatePaymentServiceUserRequest) GetAddress() *V3AddressRequest { + if o == nil { + return nil + } + return o.Address +} + +func (o *V3CreatePaymentServiceUserRequest) GetBankAccountIDs() []string { + if o == nil { + return nil + } + return o.BankAccountIDs +} + +func (o *V3CreatePaymentServiceUserRequest) GetMetadata() map[string]string { + if o == nil { + return nil + } + return o.Metadata +} diff --git a/pkg/client/models/components/v3createpaymentserviceuserresponse.go b/pkg/client/models/components/v3createpaymentserviceuserresponse.go new file mode 100644 index 000000000..39119f809 --- /dev/null +++ b/pkg/client/models/components/v3createpaymentserviceuserresponse.go @@ -0,0 +1,15 @@ +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package components + +type V3CreatePaymentServiceUserResponse struct { + // The ID of the created payment service user + Data string `json:"data"` +} + +func (o *V3CreatePaymentServiceUserResponse) GetData() string { + if o == nil { + return "" + } + return o.Data +} diff --git a/pkg/client/models/components/v3forwardpaymentserviceuserbankaccountrequest.go b/pkg/client/models/components/v3forwardpaymentserviceuserbankaccountrequest.go new file mode 100644 index 000000000..40b66d625 --- /dev/null +++ b/pkg/client/models/components/v3forwardpaymentserviceuserbankaccountrequest.go @@ -0,0 +1,14 @@ +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package components + +type V3ForwardPaymentServiceUserBankAccountRequest struct { + ConnectorID string `json:"connectorID"` +} + +func (o *V3ForwardPaymentServiceUserBankAccountRequest) GetConnectorID() string { + if o == nil { + return "" + } + return o.ConnectorID +} diff --git a/pkg/client/models/components/v3forwardpaymentserviceuserbankaccountresponse.go b/pkg/client/models/components/v3forwardpaymentserviceuserbankaccountresponse.go new file mode 100644 index 000000000..5788c305d --- /dev/null +++ b/pkg/client/models/components/v3forwardpaymentserviceuserbankaccountresponse.go @@ -0,0 +1,27 @@ +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package components + +type V3ForwardPaymentServiceUserBankAccountResponseData struct { + // Since this call is asynchronous, the response will contain the ID of the task that was created to forward the bank account to the PSP. You can use the task API to check the status of the task and get the resulting bank account ID. + // + TaskID string `json:"taskID"` +} + +func (o *V3ForwardPaymentServiceUserBankAccountResponseData) GetTaskID() string { + if o == nil { + return "" + } + return o.TaskID +} + +type V3ForwardPaymentServiceUserBankAccountResponse struct { + Data V3ForwardPaymentServiceUserBankAccountResponseData `json:"data"` +} + +func (o *V3ForwardPaymentServiceUserBankAccountResponse) GetData() V3ForwardPaymentServiceUserBankAccountResponseData { + if o == nil { + return V3ForwardPaymentServiceUserBankAccountResponseData{} + } + return o.Data +} diff --git a/pkg/client/models/components/v3getpaymentserviceuserresponse.go b/pkg/client/models/components/v3getpaymentserviceuserresponse.go new file mode 100644 index 000000000..7c4de7800 --- /dev/null +++ b/pkg/client/models/components/v3getpaymentserviceuserresponse.go @@ -0,0 +1,14 @@ +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package components + +type V3GetPaymentServiceUserResponse struct { + Data V3PaymentServiceUser `json:"data"` +} + +func (o *V3GetPaymentServiceUserResponse) GetData() V3PaymentServiceUser { + if o == nil { + return V3PaymentServiceUser{} + } + return o.Data +} diff --git a/pkg/client/models/components/v3paymentserviceuser.go b/pkg/client/models/components/v3paymentserviceuser.go new file mode 100644 index 000000000..087a04223 --- /dev/null +++ b/pkg/client/models/components/v3paymentserviceuser.go @@ -0,0 +1,78 @@ +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package components + +import ( + "github.com/formancehq/payments/pkg/client/internal/utils" + "time" +) + +type V3PaymentServiceUser struct { + ID string `json:"id"` + Name string `json:"name"` + CreatedAt time.Time `json:"createdAt"` + ContactDetails *V3ContactDetails `json:"contactDetails,omitempty"` + Address *V3Address `json:"address,omitempty"` + BankAccountIDs []string `json:"bankAccountIDs,omitempty"` + Metadata map[string]string `json:"metadata,omitempty"` +} + +func (v V3PaymentServiceUser) MarshalJSON() ([]byte, error) { + return utils.MarshalJSON(v, "", false) +} + +func (v *V3PaymentServiceUser) UnmarshalJSON(data []byte) error { + if err := utils.UnmarshalJSON(data, &v, "", false, false); err != nil { + return err + } + return nil +} + +func (o *V3PaymentServiceUser) GetID() string { + if o == nil { + return "" + } + return o.ID +} + +func (o *V3PaymentServiceUser) GetName() string { + if o == nil { + return "" + } + return o.Name +} + +func (o *V3PaymentServiceUser) GetCreatedAt() time.Time { + if o == nil { + return time.Time{} + } + return o.CreatedAt +} + +func (o *V3PaymentServiceUser) GetContactDetails() *V3ContactDetails { + if o == nil { + return nil + } + return o.ContactDetails +} + +func (o *V3PaymentServiceUser) GetAddress() *V3Address { + if o == nil { + return nil + } + return o.Address +} + +func (o *V3PaymentServiceUser) GetBankAccountIDs() []string { + if o == nil { + return nil + } + return o.BankAccountIDs +} + +func (o *V3PaymentServiceUser) GetMetadata() map[string]string { + if o == nil { + return nil + } + return o.Metadata +} diff --git a/pkg/client/models/components/v3paymentserviceuserscursorresponse.go b/pkg/client/models/components/v3paymentserviceuserscursorresponse.go new file mode 100644 index 000000000..1d2d03b5a --- /dev/null +++ b/pkg/client/models/components/v3paymentserviceuserscursorresponse.go @@ -0,0 +1,57 @@ +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package components + +type V3PaymentServiceUsersCursorResponseCursor struct { + PageSize int64 `json:"pageSize"` + HasMore bool `json:"hasMore"` + Previous *string `json:"previous,omitempty"` + Next *string `json:"next,omitempty"` + Data []V3PaymentServiceUser `json:"data"` +} + +func (o *V3PaymentServiceUsersCursorResponseCursor) GetPageSize() int64 { + if o == nil { + return 0 + } + return o.PageSize +} + +func (o *V3PaymentServiceUsersCursorResponseCursor) GetHasMore() bool { + if o == nil { + return false + } + return o.HasMore +} + +func (o *V3PaymentServiceUsersCursorResponseCursor) GetPrevious() *string { + if o == nil { + return nil + } + return o.Previous +} + +func (o *V3PaymentServiceUsersCursorResponseCursor) GetNext() *string { + if o == nil { + return nil + } + return o.Next +} + +func (o *V3PaymentServiceUsersCursorResponseCursor) GetData() []V3PaymentServiceUser { + if o == nil { + return []V3PaymentServiceUser{} + } + return o.Data +} + +type V3PaymentServiceUsersCursorResponse struct { + Cursor V3PaymentServiceUsersCursorResponseCursor `json:"cursor"` +} + +func (o *V3PaymentServiceUsersCursorResponse) GetCursor() V3PaymentServiceUsersCursorResponseCursor { + if o == nil { + return V3PaymentServiceUsersCursorResponseCursor{} + } + return o.Cursor +} diff --git a/pkg/client/models/operations/v3addbankaccounttopaymentserviceuser.go b/pkg/client/models/operations/v3addbankaccounttopaymentserviceuser.go new file mode 100644 index 000000000..f03b6b062 --- /dev/null +++ b/pkg/client/models/operations/v3addbankaccounttopaymentserviceuser.go @@ -0,0 +1,48 @@ +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package operations + +import ( + "github.com/formancehq/payments/pkg/client/models/components" +) + +type V3AddBankAccountToPaymentServiceUserRequest struct { + // The payment service user ID + PaymentServiceUserID string `pathParam:"style=simple,explode=false,name=paymentServiceUserID"` + // The bank account ID + BankAccountID string `pathParam:"style=simple,explode=false,name=bankAccountID"` +} + +func (o *V3AddBankAccountToPaymentServiceUserRequest) GetPaymentServiceUserID() string { + if o == nil { + return "" + } + return o.PaymentServiceUserID +} + +func (o *V3AddBankAccountToPaymentServiceUserRequest) GetBankAccountID() string { + if o == nil { + return "" + } + return o.BankAccountID +} + +type V3AddBankAccountToPaymentServiceUserResponse struct { + HTTPMeta components.HTTPMetadata `json:"-"` + // Error + V3ErrorResponse *components.V3ErrorResponse +} + +func (o *V3AddBankAccountToPaymentServiceUserResponse) GetHTTPMeta() components.HTTPMetadata { + if o == nil { + return components.HTTPMetadata{} + } + return o.HTTPMeta +} + +func (o *V3AddBankAccountToPaymentServiceUserResponse) GetV3ErrorResponse() *components.V3ErrorResponse { + if o == nil { + return nil + } + return o.V3ErrorResponse +} diff --git a/pkg/client/models/operations/v3createpaymentserviceuser.go b/pkg/client/models/operations/v3createpaymentserviceuser.go new file mode 100644 index 000000000..62de4175a --- /dev/null +++ b/pkg/client/models/operations/v3createpaymentserviceuser.go @@ -0,0 +1,36 @@ +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package operations + +import ( + "github.com/formancehq/payments/pkg/client/models/components" +) + +type V3CreatePaymentServiceUserResponse struct { + HTTPMeta components.HTTPMetadata `json:"-"` + // Created + V3CreatePaymentServiceUserResponse *components.V3CreatePaymentServiceUserResponse + // Error + V3ErrorResponse *components.V3ErrorResponse +} + +func (o *V3CreatePaymentServiceUserResponse) GetHTTPMeta() components.HTTPMetadata { + if o == nil { + return components.HTTPMetadata{} + } + return o.HTTPMeta +} + +func (o *V3CreatePaymentServiceUserResponse) GetV3CreatePaymentServiceUserResponse() *components.V3CreatePaymentServiceUserResponse { + if o == nil { + return nil + } + return o.V3CreatePaymentServiceUserResponse +} + +func (o *V3CreatePaymentServiceUserResponse) GetV3ErrorResponse() *components.V3ErrorResponse { + if o == nil { + return nil + } + return o.V3ErrorResponse +} diff --git a/pkg/client/models/operations/v3forwardpaymentserviceuserbankaccount.go b/pkg/client/models/operations/v3forwardpaymentserviceuserbankaccount.go new file mode 100644 index 000000000..d9da70758 --- /dev/null +++ b/pkg/client/models/operations/v3forwardpaymentserviceuserbankaccount.go @@ -0,0 +1,65 @@ +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package operations + +import ( + "github.com/formancehq/payments/pkg/client/models/components" +) + +type V3ForwardPaymentServiceUserBankAccountRequest struct { + // The payment service user ID + PaymentServiceUserID string `pathParam:"style=simple,explode=false,name=paymentServiceUserID"` + // The bank account ID + BankAccountID string `pathParam:"style=simple,explode=false,name=bankAccountID"` + V3ForwardPaymentServiceUserBankAccountRequest *components.V3ForwardPaymentServiceUserBankAccountRequest `request:"mediaType=application/json"` +} + +func (o *V3ForwardPaymentServiceUserBankAccountRequest) GetPaymentServiceUserID() string { + if o == nil { + return "" + } + return o.PaymentServiceUserID +} + +func (o *V3ForwardPaymentServiceUserBankAccountRequest) GetBankAccountID() string { + if o == nil { + return "" + } + return o.BankAccountID +} + +func (o *V3ForwardPaymentServiceUserBankAccountRequest) GetV3ForwardPaymentServiceUserBankAccountRequest() *components.V3ForwardPaymentServiceUserBankAccountRequest { + if o == nil { + return nil + } + return o.V3ForwardPaymentServiceUserBankAccountRequest +} + +type V3ForwardPaymentServiceUserBankAccountResponse struct { + HTTPMeta components.HTTPMetadata `json:"-"` + // Accepted + V3ForwardPaymentServiceUserBankAccountResponse *components.V3ForwardPaymentServiceUserBankAccountResponse + // Error + V3ErrorResponse *components.V3ErrorResponse +} + +func (o *V3ForwardPaymentServiceUserBankAccountResponse) GetHTTPMeta() components.HTTPMetadata { + if o == nil { + return components.HTTPMetadata{} + } + return o.HTTPMeta +} + +func (o *V3ForwardPaymentServiceUserBankAccountResponse) GetV3ForwardPaymentServiceUserBankAccountResponse() *components.V3ForwardPaymentServiceUserBankAccountResponse { + if o == nil { + return nil + } + return o.V3ForwardPaymentServiceUserBankAccountResponse +} + +func (o *V3ForwardPaymentServiceUserBankAccountResponse) GetV3ErrorResponse() *components.V3ErrorResponse { + if o == nil { + return nil + } + return o.V3ErrorResponse +} diff --git a/pkg/client/models/operations/v3getpaymentserviceuser.go b/pkg/client/models/operations/v3getpaymentserviceuser.go new file mode 100644 index 000000000..6900e182c --- /dev/null +++ b/pkg/client/models/operations/v3getpaymentserviceuser.go @@ -0,0 +1,48 @@ +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package operations + +import ( + "github.com/formancehq/payments/pkg/client/models/components" +) + +type V3GetPaymentServiceUserRequest struct { + // The payment service user ID + PaymentServiceUserID string `pathParam:"style=simple,explode=false,name=paymentServiceUserID"` +} + +func (o *V3GetPaymentServiceUserRequest) GetPaymentServiceUserID() string { + if o == nil { + return "" + } + return o.PaymentServiceUserID +} + +type V3GetPaymentServiceUserResponse struct { + HTTPMeta components.HTTPMetadata `json:"-"` + // OK + V3GetPaymentServiceUserResponse *components.V3GetPaymentServiceUserResponse + // Error + V3ErrorResponse *components.V3ErrorResponse +} + +func (o *V3GetPaymentServiceUserResponse) GetHTTPMeta() components.HTTPMetadata { + if o == nil { + return components.HTTPMetadata{} + } + return o.HTTPMeta +} + +func (o *V3GetPaymentServiceUserResponse) GetV3GetPaymentServiceUserResponse() *components.V3GetPaymentServiceUserResponse { + if o == nil { + return nil + } + return o.V3GetPaymentServiceUserResponse +} + +func (o *V3GetPaymentServiceUserResponse) GetV3ErrorResponse() *components.V3ErrorResponse { + if o == nil { + return nil + } + return o.V3ErrorResponse +} diff --git a/pkg/client/models/operations/v3listpaymentserviceusers.go b/pkg/client/models/operations/v3listpaymentserviceusers.go new file mode 100644 index 000000000..3e739b6b5 --- /dev/null +++ b/pkg/client/models/operations/v3listpaymentserviceusers.go @@ -0,0 +1,66 @@ +// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + +package operations + +import ( + "github.com/formancehq/payments/pkg/client/models/components" +) + +type V3ListPaymentServiceUsersRequest struct { + // The number of items to return + PageSize *int64 `queryParam:"style=form,explode=true,name=pageSize"` + // Parameter used in pagination requests. Set to the value of next for the next page of results. Set to the value of previous for the previous page of results. No other parameters can be set when this parameter is set. + // + Cursor *string `queryParam:"style=form,explode=true,name=cursor"` + RequestBody map[string]any `request:"mediaType=application/json"` +} + +func (o *V3ListPaymentServiceUsersRequest) GetPageSize() *int64 { + if o == nil { + return nil + } + return o.PageSize +} + +func (o *V3ListPaymentServiceUsersRequest) GetCursor() *string { + if o == nil { + return nil + } + return o.Cursor +} + +func (o *V3ListPaymentServiceUsersRequest) GetRequestBody() map[string]any { + if o == nil { + return nil + } + return o.RequestBody +} + +type V3ListPaymentServiceUsersResponse struct { + HTTPMeta components.HTTPMetadata `json:"-"` + // OK + V3PaymentServiceUsersCursorResponse *components.V3PaymentServiceUsersCursorResponse + // Error + V3ErrorResponse *components.V3ErrorResponse +} + +func (o *V3ListPaymentServiceUsersResponse) GetHTTPMeta() components.HTTPMetadata { + if o == nil { + return components.HTTPMetadata{} + } + return o.HTTPMeta +} + +func (o *V3ListPaymentServiceUsersResponse) GetV3PaymentServiceUsersCursorResponse() *components.V3PaymentServiceUsersCursorResponse { + if o == nil { + return nil + } + return o.V3PaymentServiceUsersCursorResponse +} + +func (o *V3ListPaymentServiceUsersResponse) GetV3ErrorResponse() *components.V3ErrorResponse { + if o == nil { + return nil + } + return o.V3ErrorResponse +} diff --git a/pkg/client/v3.go b/pkg/client/v3.go index 8c3161259..c7e675fc3 100644 --- a/pkg/client/v3.go +++ b/pkg/client/v3.go @@ -7368,6 +7368,1112 @@ func (s *V3) ListPaymentInitiationRelatedPayments(ctx context.Context, paymentIn } +// CreatePaymentServiceUser - Create a formance payment service user object +func (s *V3) CreatePaymentServiceUser(ctx context.Context, request *components.V3CreatePaymentServiceUserRequest, opts ...operations.Option) (*operations.V3CreatePaymentServiceUserResponse, error) { + o := operations.Options{} + supportedOptions := []string{ + operations.SupportedOptionRetries, + operations.SupportedOptionTimeout, + } + + for _, opt := range opts { + if err := opt(&o, supportedOptions...); err != nil { + return nil, fmt.Errorf("error applying option: %w", err) + } + } + + var baseURL string + if o.ServerURL == nil { + baseURL = utils.ReplaceParameters(s.sdkConfiguration.GetServerDetails()) + } else { + baseURL = *o.ServerURL + } + opURL, err := url.JoinPath(baseURL, "/v3/payment-service-users") + if err != nil { + return nil, fmt.Errorf("error generating URL: %w", err) + } + + hookCtx := hooks.HookContext{ + BaseURL: baseURL, + Context: ctx, + OperationID: "v3CreatePaymentServiceUser", + OAuth2Scopes: nil, + SecuritySource: s.sdkConfiguration.Security, + } + bodyReader, reqContentType, err := utils.SerializeRequestBody(ctx, request, false, true, "Request", "json", `request:"mediaType=application/json"`) + if err != nil { + return nil, err + } + + timeout := o.Timeout + if timeout == nil { + timeout = s.sdkConfiguration.Timeout + } + + if timeout != nil { + var cancel context.CancelFunc + ctx, cancel = context.WithTimeout(ctx, *timeout) + defer cancel() + } + + req, err := http.NewRequestWithContext(ctx, "POST", opURL, bodyReader) + if err != nil { + return nil, fmt.Errorf("error creating request: %w", err) + } + req.Header.Set("Accept", "application/json") + req.Header.Set("User-Agent", s.sdkConfiguration.UserAgent) + if reqContentType != "" { + req.Header.Set("Content-Type", reqContentType) + } + + if err := utils.PopulateSecurity(ctx, req, s.sdkConfiguration.Security); err != nil { + return nil, err + } + + for k, v := range o.SetHeaders { + req.Header.Set(k, v) + } + + globalRetryConfig := s.sdkConfiguration.RetryConfig + retryConfig := o.Retries + if retryConfig == nil { + if globalRetryConfig != nil { + retryConfig = globalRetryConfig + } + } + + var httpRes *http.Response + if retryConfig != nil { + httpRes, err = utils.Retry(ctx, utils.Retries{ + Config: retryConfig, + StatusCodes: []string{ + "429", + "500", + "502", + "503", + "504", + }, + }, func() (*http.Response, error) { + if req.Body != nil { + copyBody, err := req.GetBody() + if err != nil { + return nil, err + } + req.Body = copyBody + } + + req, err = s.sdkConfiguration.Hooks.BeforeRequest(hooks.BeforeRequestContext{HookContext: hookCtx}, req) + if err != nil { + if retry.IsPermanentError(err) || retry.IsTemporaryError(err) { + return nil, err + } + + return nil, retry.Permanent(err) + } + + httpRes, err := s.sdkConfiguration.Client.Do(req) + if err != nil || httpRes == nil { + if err != nil { + err = fmt.Errorf("error sending request: %w", err) + } else { + err = fmt.Errorf("error sending request: no response") + } + + _, err = s.sdkConfiguration.Hooks.AfterError(hooks.AfterErrorContext{HookContext: hookCtx}, nil, err) + } + return httpRes, err + }) + + if err != nil { + return nil, err + } else { + httpRes, err = s.sdkConfiguration.Hooks.AfterSuccess(hooks.AfterSuccessContext{HookContext: hookCtx}, httpRes) + if err != nil { + return nil, err + } + } + } else { + req, err = s.sdkConfiguration.Hooks.BeforeRequest(hooks.BeforeRequestContext{HookContext: hookCtx}, req) + if err != nil { + return nil, err + } + + httpRes, err = s.sdkConfiguration.Client.Do(req) + if err != nil || httpRes == nil { + if err != nil { + err = fmt.Errorf("error sending request: %w", err) + } else { + err = fmt.Errorf("error sending request: no response") + } + + _, err = s.sdkConfiguration.Hooks.AfterError(hooks.AfterErrorContext{HookContext: hookCtx}, nil, err) + return nil, err + } else if utils.MatchStatusCodes([]string{"4XX", "5XX"}, httpRes.StatusCode) { + _httpRes, err := s.sdkConfiguration.Hooks.AfterError(hooks.AfterErrorContext{HookContext: hookCtx}, httpRes, nil) + if err != nil { + return nil, err + } else if _httpRes != nil { + httpRes = _httpRes + } + } else { + httpRes, err = s.sdkConfiguration.Hooks.AfterSuccess(hooks.AfterSuccessContext{HookContext: hookCtx}, httpRes) + if err != nil { + return nil, err + } + } + } + + res := &operations.V3CreatePaymentServiceUserResponse{ + HTTPMeta: components.HTTPMetadata{ + Request: req, + Response: httpRes, + }, + } + + switch { + case httpRes.StatusCode == 201: + switch { + case utils.MatchContentType(httpRes.Header.Get("Content-Type"), `application/json`): + rawBody, err := utils.ConsumeRawBody(httpRes) + if err != nil { + return nil, err + } + + var out components.V3CreatePaymentServiceUserResponse + if err := utils.UnmarshalJsonFromResponseBody(bytes.NewBuffer(rawBody), &out, ""); err != nil { + return nil, err + } + + res.V3CreatePaymentServiceUserResponse = &out + default: + rawBody, err := utils.ConsumeRawBody(httpRes) + if err != nil { + return nil, err + } + return nil, sdkerrors.NewSDKError(fmt.Sprintf("unknown content-type received: %s", httpRes.Header.Get("Content-Type")), httpRes.StatusCode, string(rawBody), httpRes) + } + case httpRes.StatusCode >= 400 && httpRes.StatusCode < 500: + rawBody, err := utils.ConsumeRawBody(httpRes) + if err != nil { + return nil, err + } + return nil, sdkerrors.NewSDKError("API error occurred", httpRes.StatusCode, string(rawBody), httpRes) + case httpRes.StatusCode >= 500 && httpRes.StatusCode < 600: + rawBody, err := utils.ConsumeRawBody(httpRes) + if err != nil { + return nil, err + } + return nil, sdkerrors.NewSDKError("API error occurred", httpRes.StatusCode, string(rawBody), httpRes) + default: + switch { + case utils.MatchContentType(httpRes.Header.Get("Content-Type"), `application/json`): + rawBody, err := utils.ConsumeRawBody(httpRes) + if err != nil { + return nil, err + } + + var out components.V3ErrorResponse + if err := utils.UnmarshalJsonFromResponseBody(bytes.NewBuffer(rawBody), &out, ""); err != nil { + return nil, err + } + + res.V3ErrorResponse = &out + default: + rawBody, err := utils.ConsumeRawBody(httpRes) + if err != nil { + return nil, err + } + return nil, sdkerrors.NewSDKError(fmt.Sprintf("unknown content-type received: %s", httpRes.Header.Get("Content-Type")), httpRes.StatusCode, string(rawBody), httpRes) + } + } + + return res, nil + +} + +// ListPaymentServiceUsers - List all payment service users +func (s *V3) ListPaymentServiceUsers(ctx context.Context, pageSize *int64, cursor *string, requestBody map[string]any, opts ...operations.Option) (*operations.V3ListPaymentServiceUsersResponse, error) { + request := operations.V3ListPaymentServiceUsersRequest{ + PageSize: pageSize, + Cursor: cursor, + RequestBody: requestBody, + } + + o := operations.Options{} + supportedOptions := []string{ + operations.SupportedOptionRetries, + operations.SupportedOptionTimeout, + } + + for _, opt := range opts { + if err := opt(&o, supportedOptions...); err != nil { + return nil, fmt.Errorf("error applying option: %w", err) + } + } + + var baseURL string + if o.ServerURL == nil { + baseURL = utils.ReplaceParameters(s.sdkConfiguration.GetServerDetails()) + } else { + baseURL = *o.ServerURL + } + opURL, err := url.JoinPath(baseURL, "/v3/payment-service-users") + if err != nil { + return nil, fmt.Errorf("error generating URL: %w", err) + } + + hookCtx := hooks.HookContext{ + BaseURL: baseURL, + Context: ctx, + OperationID: "v3ListPaymentServiceUsers", + OAuth2Scopes: nil, + SecuritySource: s.sdkConfiguration.Security, + } + bodyReader, reqContentType, err := utils.SerializeRequestBody(ctx, request, false, true, "RequestBody", "json", `request:"mediaType=application/json"`) + if err != nil { + return nil, err + } + + timeout := o.Timeout + if timeout == nil { + timeout = s.sdkConfiguration.Timeout + } + + if timeout != nil { + var cancel context.CancelFunc + ctx, cancel = context.WithTimeout(ctx, *timeout) + defer cancel() + } + + req, err := http.NewRequestWithContext(ctx, "GET", opURL, bodyReader) + if err != nil { + return nil, fmt.Errorf("error creating request: %w", err) + } + req.Header.Set("Accept", "application/json") + req.Header.Set("User-Agent", s.sdkConfiguration.UserAgent) + if reqContentType != "" { + req.Header.Set("Content-Type", reqContentType) + } + + if err := utils.PopulateQueryParams(ctx, req, request, nil); err != nil { + return nil, fmt.Errorf("error populating query params: %w", err) + } + + if err := utils.PopulateSecurity(ctx, req, s.sdkConfiguration.Security); err != nil { + return nil, err + } + + for k, v := range o.SetHeaders { + req.Header.Set(k, v) + } + + globalRetryConfig := s.sdkConfiguration.RetryConfig + retryConfig := o.Retries + if retryConfig == nil { + if globalRetryConfig != nil { + retryConfig = globalRetryConfig + } + } + + var httpRes *http.Response + if retryConfig != nil { + httpRes, err = utils.Retry(ctx, utils.Retries{ + Config: retryConfig, + StatusCodes: []string{ + "429", + "500", + "502", + "503", + "504", + }, + }, func() (*http.Response, error) { + if req.Body != nil { + copyBody, err := req.GetBody() + if err != nil { + return nil, err + } + req.Body = copyBody + } + + req, err = s.sdkConfiguration.Hooks.BeforeRequest(hooks.BeforeRequestContext{HookContext: hookCtx}, req) + if err != nil { + if retry.IsPermanentError(err) || retry.IsTemporaryError(err) { + return nil, err + } + + return nil, retry.Permanent(err) + } + + httpRes, err := s.sdkConfiguration.Client.Do(req) + if err != nil || httpRes == nil { + if err != nil { + err = fmt.Errorf("error sending request: %w", err) + } else { + err = fmt.Errorf("error sending request: no response") + } + + _, err = s.sdkConfiguration.Hooks.AfterError(hooks.AfterErrorContext{HookContext: hookCtx}, nil, err) + } + return httpRes, err + }) + + if err != nil { + return nil, err + } else { + httpRes, err = s.sdkConfiguration.Hooks.AfterSuccess(hooks.AfterSuccessContext{HookContext: hookCtx}, httpRes) + if err != nil { + return nil, err + } + } + } else { + req, err = s.sdkConfiguration.Hooks.BeforeRequest(hooks.BeforeRequestContext{HookContext: hookCtx}, req) + if err != nil { + return nil, err + } + + httpRes, err = s.sdkConfiguration.Client.Do(req) + if err != nil || httpRes == nil { + if err != nil { + err = fmt.Errorf("error sending request: %w", err) + } else { + err = fmt.Errorf("error sending request: no response") + } + + _, err = s.sdkConfiguration.Hooks.AfterError(hooks.AfterErrorContext{HookContext: hookCtx}, nil, err) + return nil, err + } else if utils.MatchStatusCodes([]string{"4XX", "5XX"}, httpRes.StatusCode) { + _httpRes, err := s.sdkConfiguration.Hooks.AfterError(hooks.AfterErrorContext{HookContext: hookCtx}, httpRes, nil) + if err != nil { + return nil, err + } else if _httpRes != nil { + httpRes = _httpRes + } + } else { + httpRes, err = s.sdkConfiguration.Hooks.AfterSuccess(hooks.AfterSuccessContext{HookContext: hookCtx}, httpRes) + if err != nil { + return nil, err + } + } + } + + res := &operations.V3ListPaymentServiceUsersResponse{ + HTTPMeta: components.HTTPMetadata{ + Request: req, + Response: httpRes, + }, + } + + switch { + case httpRes.StatusCode == 200: + switch { + case utils.MatchContentType(httpRes.Header.Get("Content-Type"), `application/json`): + rawBody, err := utils.ConsumeRawBody(httpRes) + if err != nil { + return nil, err + } + + var out components.V3PaymentServiceUsersCursorResponse + if err := utils.UnmarshalJsonFromResponseBody(bytes.NewBuffer(rawBody), &out, ""); err != nil { + return nil, err + } + + res.V3PaymentServiceUsersCursorResponse = &out + default: + rawBody, err := utils.ConsumeRawBody(httpRes) + if err != nil { + return nil, err + } + return nil, sdkerrors.NewSDKError(fmt.Sprintf("unknown content-type received: %s", httpRes.Header.Get("Content-Type")), httpRes.StatusCode, string(rawBody), httpRes) + } + case httpRes.StatusCode >= 400 && httpRes.StatusCode < 500: + rawBody, err := utils.ConsumeRawBody(httpRes) + if err != nil { + return nil, err + } + return nil, sdkerrors.NewSDKError("API error occurred", httpRes.StatusCode, string(rawBody), httpRes) + case httpRes.StatusCode >= 500 && httpRes.StatusCode < 600: + rawBody, err := utils.ConsumeRawBody(httpRes) + if err != nil { + return nil, err + } + return nil, sdkerrors.NewSDKError("API error occurred", httpRes.StatusCode, string(rawBody), httpRes) + default: + switch { + case utils.MatchContentType(httpRes.Header.Get("Content-Type"), `application/json`): + rawBody, err := utils.ConsumeRawBody(httpRes) + if err != nil { + return nil, err + } + + var out components.V3ErrorResponse + if err := utils.UnmarshalJsonFromResponseBody(bytes.NewBuffer(rawBody), &out, ""); err != nil { + return nil, err + } + + res.V3ErrorResponse = &out + default: + rawBody, err := utils.ConsumeRawBody(httpRes) + if err != nil { + return nil, err + } + return nil, sdkerrors.NewSDKError(fmt.Sprintf("unknown content-type received: %s", httpRes.Header.Get("Content-Type")), httpRes.StatusCode, string(rawBody), httpRes) + } + } + + return res, nil + +} + +// GetPaymentServiceUser - Get a payment service user by ID +func (s *V3) GetPaymentServiceUser(ctx context.Context, paymentServiceUserID string, opts ...operations.Option) (*operations.V3GetPaymentServiceUserResponse, error) { + request := operations.V3GetPaymentServiceUserRequest{ + PaymentServiceUserID: paymentServiceUserID, + } + + o := operations.Options{} + supportedOptions := []string{ + operations.SupportedOptionRetries, + operations.SupportedOptionTimeout, + } + + for _, opt := range opts { + if err := opt(&o, supportedOptions...); err != nil { + return nil, fmt.Errorf("error applying option: %w", err) + } + } + + var baseURL string + if o.ServerURL == nil { + baseURL = utils.ReplaceParameters(s.sdkConfiguration.GetServerDetails()) + } else { + baseURL = *o.ServerURL + } + opURL, err := utils.GenerateURL(ctx, baseURL, "/v3/payment-service-users/{paymentServiceUserID}", request, nil) + if err != nil { + return nil, fmt.Errorf("error generating URL: %w", err) + } + + hookCtx := hooks.HookContext{ + BaseURL: baseURL, + Context: ctx, + OperationID: "v3GetPaymentServiceUser", + OAuth2Scopes: nil, + SecuritySource: s.sdkConfiguration.Security, + } + + timeout := o.Timeout + if timeout == nil { + timeout = s.sdkConfiguration.Timeout + } + + if timeout != nil { + var cancel context.CancelFunc + ctx, cancel = context.WithTimeout(ctx, *timeout) + defer cancel() + } + + req, err := http.NewRequestWithContext(ctx, "GET", opURL, nil) + if err != nil { + return nil, fmt.Errorf("error creating request: %w", err) + } + req.Header.Set("Accept", "application/json") + req.Header.Set("User-Agent", s.sdkConfiguration.UserAgent) + + if err := utils.PopulateSecurity(ctx, req, s.sdkConfiguration.Security); err != nil { + return nil, err + } + + for k, v := range o.SetHeaders { + req.Header.Set(k, v) + } + + globalRetryConfig := s.sdkConfiguration.RetryConfig + retryConfig := o.Retries + if retryConfig == nil { + if globalRetryConfig != nil { + retryConfig = globalRetryConfig + } + } + + var httpRes *http.Response + if retryConfig != nil { + httpRes, err = utils.Retry(ctx, utils.Retries{ + Config: retryConfig, + StatusCodes: []string{ + "429", + "500", + "502", + "503", + "504", + }, + }, func() (*http.Response, error) { + if req.Body != nil { + copyBody, err := req.GetBody() + if err != nil { + return nil, err + } + req.Body = copyBody + } + + req, err = s.sdkConfiguration.Hooks.BeforeRequest(hooks.BeforeRequestContext{HookContext: hookCtx}, req) + if err != nil { + if retry.IsPermanentError(err) || retry.IsTemporaryError(err) { + return nil, err + } + + return nil, retry.Permanent(err) + } + + httpRes, err := s.sdkConfiguration.Client.Do(req) + if err != nil || httpRes == nil { + if err != nil { + err = fmt.Errorf("error sending request: %w", err) + } else { + err = fmt.Errorf("error sending request: no response") + } + + _, err = s.sdkConfiguration.Hooks.AfterError(hooks.AfterErrorContext{HookContext: hookCtx}, nil, err) + } + return httpRes, err + }) + + if err != nil { + return nil, err + } else { + httpRes, err = s.sdkConfiguration.Hooks.AfterSuccess(hooks.AfterSuccessContext{HookContext: hookCtx}, httpRes) + if err != nil { + return nil, err + } + } + } else { + req, err = s.sdkConfiguration.Hooks.BeforeRequest(hooks.BeforeRequestContext{HookContext: hookCtx}, req) + if err != nil { + return nil, err + } + + httpRes, err = s.sdkConfiguration.Client.Do(req) + if err != nil || httpRes == nil { + if err != nil { + err = fmt.Errorf("error sending request: %w", err) + } else { + err = fmt.Errorf("error sending request: no response") + } + + _, err = s.sdkConfiguration.Hooks.AfterError(hooks.AfterErrorContext{HookContext: hookCtx}, nil, err) + return nil, err + } else if utils.MatchStatusCodes([]string{"4XX", "5XX"}, httpRes.StatusCode) { + _httpRes, err := s.sdkConfiguration.Hooks.AfterError(hooks.AfterErrorContext{HookContext: hookCtx}, httpRes, nil) + if err != nil { + return nil, err + } else if _httpRes != nil { + httpRes = _httpRes + } + } else { + httpRes, err = s.sdkConfiguration.Hooks.AfterSuccess(hooks.AfterSuccessContext{HookContext: hookCtx}, httpRes) + if err != nil { + return nil, err + } + } + } + + res := &operations.V3GetPaymentServiceUserResponse{ + HTTPMeta: components.HTTPMetadata{ + Request: req, + Response: httpRes, + }, + } + + switch { + case httpRes.StatusCode == 200: + switch { + case utils.MatchContentType(httpRes.Header.Get("Content-Type"), `application/json`): + rawBody, err := utils.ConsumeRawBody(httpRes) + if err != nil { + return nil, err + } + + var out components.V3GetPaymentServiceUserResponse + if err := utils.UnmarshalJsonFromResponseBody(bytes.NewBuffer(rawBody), &out, ""); err != nil { + return nil, err + } + + res.V3GetPaymentServiceUserResponse = &out + default: + rawBody, err := utils.ConsumeRawBody(httpRes) + if err != nil { + return nil, err + } + return nil, sdkerrors.NewSDKError(fmt.Sprintf("unknown content-type received: %s", httpRes.Header.Get("Content-Type")), httpRes.StatusCode, string(rawBody), httpRes) + } + case httpRes.StatusCode >= 400 && httpRes.StatusCode < 500: + rawBody, err := utils.ConsumeRawBody(httpRes) + if err != nil { + return nil, err + } + return nil, sdkerrors.NewSDKError("API error occurred", httpRes.StatusCode, string(rawBody), httpRes) + case httpRes.StatusCode >= 500 && httpRes.StatusCode < 600: + rawBody, err := utils.ConsumeRawBody(httpRes) + if err != nil { + return nil, err + } + return nil, sdkerrors.NewSDKError("API error occurred", httpRes.StatusCode, string(rawBody), httpRes) + default: + switch { + case utils.MatchContentType(httpRes.Header.Get("Content-Type"), `application/json`): + rawBody, err := utils.ConsumeRawBody(httpRes) + if err != nil { + return nil, err + } + + var out components.V3ErrorResponse + if err := utils.UnmarshalJsonFromResponseBody(bytes.NewBuffer(rawBody), &out, ""); err != nil { + return nil, err + } + + res.V3ErrorResponse = &out + default: + rawBody, err := utils.ConsumeRawBody(httpRes) + if err != nil { + return nil, err + } + return nil, sdkerrors.NewSDKError(fmt.Sprintf("unknown content-type received: %s", httpRes.Header.Get("Content-Type")), httpRes.StatusCode, string(rawBody), httpRes) + } + } + + return res, nil + +} + +// AddBankAccountToPaymentServiceUser - Add a bank account to a payment service user +func (s *V3) AddBankAccountToPaymentServiceUser(ctx context.Context, paymentServiceUserID string, bankAccountID string, opts ...operations.Option) (*operations.V3AddBankAccountToPaymentServiceUserResponse, error) { + request := operations.V3AddBankAccountToPaymentServiceUserRequest{ + PaymentServiceUserID: paymentServiceUserID, + BankAccountID: bankAccountID, + } + + o := operations.Options{} + supportedOptions := []string{ + operations.SupportedOptionRetries, + operations.SupportedOptionTimeout, + } + + for _, opt := range opts { + if err := opt(&o, supportedOptions...); err != nil { + return nil, fmt.Errorf("error applying option: %w", err) + } + } + + var baseURL string + if o.ServerURL == nil { + baseURL = utils.ReplaceParameters(s.sdkConfiguration.GetServerDetails()) + } else { + baseURL = *o.ServerURL + } + opURL, err := utils.GenerateURL(ctx, baseURL, "/v3/payment-service-users/{paymentServiceUserID}/bank-accounts/{bankAccountID}", request, nil) + if err != nil { + return nil, fmt.Errorf("error generating URL: %w", err) + } + + hookCtx := hooks.HookContext{ + BaseURL: baseURL, + Context: ctx, + OperationID: "v3AddBankAccountToPaymentServiceUser", + OAuth2Scopes: nil, + SecuritySource: s.sdkConfiguration.Security, + } + + timeout := o.Timeout + if timeout == nil { + timeout = s.sdkConfiguration.Timeout + } + + if timeout != nil { + var cancel context.CancelFunc + ctx, cancel = context.WithTimeout(ctx, *timeout) + defer cancel() + } + + req, err := http.NewRequestWithContext(ctx, "POST", opURL, nil) + if err != nil { + return nil, fmt.Errorf("error creating request: %w", err) + } + req.Header.Set("Accept", "application/json") + req.Header.Set("User-Agent", s.sdkConfiguration.UserAgent) + + if err := utils.PopulateSecurity(ctx, req, s.sdkConfiguration.Security); err != nil { + return nil, err + } + + for k, v := range o.SetHeaders { + req.Header.Set(k, v) + } + + globalRetryConfig := s.sdkConfiguration.RetryConfig + retryConfig := o.Retries + if retryConfig == nil { + if globalRetryConfig != nil { + retryConfig = globalRetryConfig + } + } + + var httpRes *http.Response + if retryConfig != nil { + httpRes, err = utils.Retry(ctx, utils.Retries{ + Config: retryConfig, + StatusCodes: []string{ + "429", + "500", + "502", + "503", + "504", + }, + }, func() (*http.Response, error) { + if req.Body != nil { + copyBody, err := req.GetBody() + if err != nil { + return nil, err + } + req.Body = copyBody + } + + req, err = s.sdkConfiguration.Hooks.BeforeRequest(hooks.BeforeRequestContext{HookContext: hookCtx}, req) + if err != nil { + if retry.IsPermanentError(err) || retry.IsTemporaryError(err) { + return nil, err + } + + return nil, retry.Permanent(err) + } + + httpRes, err := s.sdkConfiguration.Client.Do(req) + if err != nil || httpRes == nil { + if err != nil { + err = fmt.Errorf("error sending request: %w", err) + } else { + err = fmt.Errorf("error sending request: no response") + } + + _, err = s.sdkConfiguration.Hooks.AfterError(hooks.AfterErrorContext{HookContext: hookCtx}, nil, err) + } + return httpRes, err + }) + + if err != nil { + return nil, err + } else { + httpRes, err = s.sdkConfiguration.Hooks.AfterSuccess(hooks.AfterSuccessContext{HookContext: hookCtx}, httpRes) + if err != nil { + return nil, err + } + } + } else { + req, err = s.sdkConfiguration.Hooks.BeforeRequest(hooks.BeforeRequestContext{HookContext: hookCtx}, req) + if err != nil { + return nil, err + } + + httpRes, err = s.sdkConfiguration.Client.Do(req) + if err != nil || httpRes == nil { + if err != nil { + err = fmt.Errorf("error sending request: %w", err) + } else { + err = fmt.Errorf("error sending request: no response") + } + + _, err = s.sdkConfiguration.Hooks.AfterError(hooks.AfterErrorContext{HookContext: hookCtx}, nil, err) + return nil, err + } else if utils.MatchStatusCodes([]string{"4XX", "5XX"}, httpRes.StatusCode) { + _httpRes, err := s.sdkConfiguration.Hooks.AfterError(hooks.AfterErrorContext{HookContext: hookCtx}, httpRes, nil) + if err != nil { + return nil, err + } else if _httpRes != nil { + httpRes = _httpRes + } + } else { + httpRes, err = s.sdkConfiguration.Hooks.AfterSuccess(hooks.AfterSuccessContext{HookContext: hookCtx}, httpRes) + if err != nil { + return nil, err + } + } + } + + res := &operations.V3AddBankAccountToPaymentServiceUserResponse{ + HTTPMeta: components.HTTPMetadata{ + Request: req, + Response: httpRes, + }, + } + + switch { + case httpRes.StatusCode == 204: + case httpRes.StatusCode >= 400 && httpRes.StatusCode < 500: + rawBody, err := utils.ConsumeRawBody(httpRes) + if err != nil { + return nil, err + } + return nil, sdkerrors.NewSDKError("API error occurred", httpRes.StatusCode, string(rawBody), httpRes) + case httpRes.StatusCode >= 500 && httpRes.StatusCode < 600: + rawBody, err := utils.ConsumeRawBody(httpRes) + if err != nil { + return nil, err + } + return nil, sdkerrors.NewSDKError("API error occurred", httpRes.StatusCode, string(rawBody), httpRes) + default: + switch { + case utils.MatchContentType(httpRes.Header.Get("Content-Type"), `application/json`): + rawBody, err := utils.ConsumeRawBody(httpRes) + if err != nil { + return nil, err + } + + var out components.V3ErrorResponse + if err := utils.UnmarshalJsonFromResponseBody(bytes.NewBuffer(rawBody), &out, ""); err != nil { + return nil, err + } + + res.V3ErrorResponse = &out + default: + rawBody, err := utils.ConsumeRawBody(httpRes) + if err != nil { + return nil, err + } + return nil, sdkerrors.NewSDKError(fmt.Sprintf("unknown content-type received: %s", httpRes.Header.Get("Content-Type")), httpRes.StatusCode, string(rawBody), httpRes) + } + } + + return res, nil + +} + +// ForwardPaymentServiceUserBankAccount - Forward a payment service user's bank account to a connector +func (s *V3) ForwardPaymentServiceUserBankAccount(ctx context.Context, paymentServiceUserID string, bankAccountID string, v3ForwardPaymentServiceUserBankAccountRequest *components.V3ForwardPaymentServiceUserBankAccountRequest, opts ...operations.Option) (*operations.V3ForwardPaymentServiceUserBankAccountResponse, error) { + request := operations.V3ForwardPaymentServiceUserBankAccountRequest{ + PaymentServiceUserID: paymentServiceUserID, + BankAccountID: bankAccountID, + V3ForwardPaymentServiceUserBankAccountRequest: v3ForwardPaymentServiceUserBankAccountRequest, + } + + o := operations.Options{} + supportedOptions := []string{ + operations.SupportedOptionRetries, + operations.SupportedOptionTimeout, + } + + for _, opt := range opts { + if err := opt(&o, supportedOptions...); err != nil { + return nil, fmt.Errorf("error applying option: %w", err) + } + } + + var baseURL string + if o.ServerURL == nil { + baseURL = utils.ReplaceParameters(s.sdkConfiguration.GetServerDetails()) + } else { + baseURL = *o.ServerURL + } + opURL, err := utils.GenerateURL(ctx, baseURL, "/v3/payment-service-users/{paymentServiceUserID}/bank-accounts/{bankAccountID}/forward", request, nil) + if err != nil { + return nil, fmt.Errorf("error generating URL: %w", err) + } + + hookCtx := hooks.HookContext{ + BaseURL: baseURL, + Context: ctx, + OperationID: "v3ForwardPaymentServiceUserBankAccount", + OAuth2Scopes: nil, + SecuritySource: s.sdkConfiguration.Security, + } + bodyReader, reqContentType, err := utils.SerializeRequestBody(ctx, request, false, true, "V3ForwardPaymentServiceUserBankAccountRequest", "json", `request:"mediaType=application/json"`) + if err != nil { + return nil, err + } + + timeout := o.Timeout + if timeout == nil { + timeout = s.sdkConfiguration.Timeout + } + + if timeout != nil { + var cancel context.CancelFunc + ctx, cancel = context.WithTimeout(ctx, *timeout) + defer cancel() + } + + req, err := http.NewRequestWithContext(ctx, "POST", opURL, bodyReader) + if err != nil { + return nil, fmt.Errorf("error creating request: %w", err) + } + req.Header.Set("Accept", "application/json") + req.Header.Set("User-Agent", s.sdkConfiguration.UserAgent) + if reqContentType != "" { + req.Header.Set("Content-Type", reqContentType) + } + + if err := utils.PopulateSecurity(ctx, req, s.sdkConfiguration.Security); err != nil { + return nil, err + } + + for k, v := range o.SetHeaders { + req.Header.Set(k, v) + } + + globalRetryConfig := s.sdkConfiguration.RetryConfig + retryConfig := o.Retries + if retryConfig == nil { + if globalRetryConfig != nil { + retryConfig = globalRetryConfig + } + } + + var httpRes *http.Response + if retryConfig != nil { + httpRes, err = utils.Retry(ctx, utils.Retries{ + Config: retryConfig, + StatusCodes: []string{ + "429", + "500", + "502", + "503", + "504", + }, + }, func() (*http.Response, error) { + if req.Body != nil { + copyBody, err := req.GetBody() + if err != nil { + return nil, err + } + req.Body = copyBody + } + + req, err = s.sdkConfiguration.Hooks.BeforeRequest(hooks.BeforeRequestContext{HookContext: hookCtx}, req) + if err != nil { + if retry.IsPermanentError(err) || retry.IsTemporaryError(err) { + return nil, err + } + + return nil, retry.Permanent(err) + } + + httpRes, err := s.sdkConfiguration.Client.Do(req) + if err != nil || httpRes == nil { + if err != nil { + err = fmt.Errorf("error sending request: %w", err) + } else { + err = fmt.Errorf("error sending request: no response") + } + + _, err = s.sdkConfiguration.Hooks.AfterError(hooks.AfterErrorContext{HookContext: hookCtx}, nil, err) + } + return httpRes, err + }) + + if err != nil { + return nil, err + } else { + httpRes, err = s.sdkConfiguration.Hooks.AfterSuccess(hooks.AfterSuccessContext{HookContext: hookCtx}, httpRes) + if err != nil { + return nil, err + } + } + } else { + req, err = s.sdkConfiguration.Hooks.BeforeRequest(hooks.BeforeRequestContext{HookContext: hookCtx}, req) + if err != nil { + return nil, err + } + + httpRes, err = s.sdkConfiguration.Client.Do(req) + if err != nil || httpRes == nil { + if err != nil { + err = fmt.Errorf("error sending request: %w", err) + } else { + err = fmt.Errorf("error sending request: no response") + } + + _, err = s.sdkConfiguration.Hooks.AfterError(hooks.AfterErrorContext{HookContext: hookCtx}, nil, err) + return nil, err + } else if utils.MatchStatusCodes([]string{"4XX", "5XX"}, httpRes.StatusCode) { + _httpRes, err := s.sdkConfiguration.Hooks.AfterError(hooks.AfterErrorContext{HookContext: hookCtx}, httpRes, nil) + if err != nil { + return nil, err + } else if _httpRes != nil { + httpRes = _httpRes + } + } else { + httpRes, err = s.sdkConfiguration.Hooks.AfterSuccess(hooks.AfterSuccessContext{HookContext: hookCtx}, httpRes) + if err != nil { + return nil, err + } + } + } + + res := &operations.V3ForwardPaymentServiceUserBankAccountResponse{ + HTTPMeta: components.HTTPMetadata{ + Request: req, + Response: httpRes, + }, + } + + switch { + case httpRes.StatusCode == 202: + switch { + case utils.MatchContentType(httpRes.Header.Get("Content-Type"), `application/json`): + rawBody, err := utils.ConsumeRawBody(httpRes) + if err != nil { + return nil, err + } + + var out components.V3ForwardPaymentServiceUserBankAccountResponse + if err := utils.UnmarshalJsonFromResponseBody(bytes.NewBuffer(rawBody), &out, ""); err != nil { + return nil, err + } + + res.V3ForwardPaymentServiceUserBankAccountResponse = &out + default: + rawBody, err := utils.ConsumeRawBody(httpRes) + if err != nil { + return nil, err + } + return nil, sdkerrors.NewSDKError(fmt.Sprintf("unknown content-type received: %s", httpRes.Header.Get("Content-Type")), httpRes.StatusCode, string(rawBody), httpRes) + } + case httpRes.StatusCode >= 400 && httpRes.StatusCode < 500: + rawBody, err := utils.ConsumeRawBody(httpRes) + if err != nil { + return nil, err + } + return nil, sdkerrors.NewSDKError("API error occurred", httpRes.StatusCode, string(rawBody), httpRes) + case httpRes.StatusCode >= 500 && httpRes.StatusCode < 600: + rawBody, err := utils.ConsumeRawBody(httpRes) + if err != nil { + return nil, err + } + return nil, sdkerrors.NewSDKError("API error occurred", httpRes.StatusCode, string(rawBody), httpRes) + default: + switch { + case utils.MatchContentType(httpRes.Header.Get("Content-Type"), `application/json`): + rawBody, err := utils.ConsumeRawBody(httpRes) + if err != nil { + return nil, err + } + + var out components.V3ErrorResponse + if err := utils.UnmarshalJsonFromResponseBody(bytes.NewBuffer(rawBody), &out, ""); err != nil { + return nil, err + } + + res.V3ErrorResponse = &out + default: + rawBody, err := utils.ConsumeRawBody(httpRes) + if err != nil { + return nil, err + } + return nil, sdkerrors.NewSDKError(fmt.Sprintf("unknown content-type received: %s", httpRes.Header.Get("Content-Type")), httpRes.StatusCode, string(rawBody), httpRes) + } + } + + return res, nil + +} + // CreatePool - Create a formance pool object func (s *V3) CreatePool(ctx context.Context, request *components.V3CreatePoolRequest, opts ...operations.Option) (*operations.V3CreatePoolResponse, error) { o := operations.Options{} diff --git a/test/e2e/api_payment_service_users_test.go b/test/e2e/api_payment_service_users_test.go new file mode 100644 index 000000000..5e1912cee --- /dev/null +++ b/test/e2e/api_payment_service_users_test.go @@ -0,0 +1,254 @@ +package test_suite + +import ( + "fmt" + + "github.com/formancehq/go-libs/v3/logging" + "github.com/formancehq/go-libs/v3/pointer" + "github.com/formancehq/go-libs/v3/testing/deferred" + "github.com/formancehq/payments/internal/events" + "github.com/formancehq/payments/internal/models" + "github.com/formancehq/payments/pkg/client/models/components" + evts "github.com/formancehq/payments/pkg/events" + "github.com/google/uuid" + "github.com/nats-io/nats.go" + + . "github.com/formancehq/payments/pkg/testserver" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +var _ = Context("Payment API Payment Service Users", func() { + var ( + db = UseTemplatedDatabase() + ctx = logging.TestingContext() + + v3CreateRequest *components.V3CreatePaymentServiceUserRequest + + baID1 uuid.UUID + baID2 uuid.UUID + + app *deferred.Deferred[*Server] + ) + + app = NewTestServer(func() Configuration { + return Configuration{ + Stack: stack, + NatsURL: natsServer.GetValue().ClientURL(), + PostgresConfiguration: db.GetValue().ConnectionOptions(), + TemporalNamespace: temporalServer.GetValue().DefaultNamespace(), + TemporalAddress: temporalServer.GetValue().Address(), + Output: GinkgoWriter, + } + }) + + v3CreateRequest = &components.V3CreatePaymentServiceUserRequest{ + Name: "test", + ContactDetails: &components.V3ContactDetailsRequest{ + Email: pointer.For("test"), + PhoneNuber: pointer.For("test"), + }, + Address: &components.V3AddressRequest{ + StreetNumber: pointer.For("1"), + StreetName: pointer.For("test"), + City: pointer.For("test"), + Region: pointer.For("test"), + PostalCode: pointer.For("test"), + Country: pointer.For("FR"), + }, + BankAccountIDs: []string{}, + Metadata: map[string]string{}, + } + + BeforeEach(func() { + createResponse, err := app.GetValue().SDK().Payments.V3.CreateBankAccount(ctx, &components.V3CreateBankAccountRequest{ + Name: "foo", + AccountNumber: pointer.For("123456789"), + Iban: pointer.For("DE89370400440532013000"), + Country: pointer.For("DE"), + }) + Expect(err).To(BeNil()) + baID1, err = uuid.Parse(createResponse.GetV3CreateBankAccountResponse().Data) + Expect(err).To(BeNil()) + + createResponse, err = app.GetValue().SDK().Payments.V3.CreateBankAccount(ctx, &components.V3CreateBankAccountRequest{ + Name: "bar", + AccountNumber: pointer.For("123456789"), + Iban: pointer.For("DE89370400440532013000"), + Country: pointer.For("DE"), + }) + Expect(err).To(BeNil()) + baID2, err = uuid.Parse(createResponse.GetV3CreateBankAccountResponse().Data) + Expect(err).To(BeNil()) + + // Only add the first bank account to the request, the second one will be added via the api + v3CreateRequest.BankAccountIDs = []string{baID1.String()} + _ = baID2 + + }) + + When("creating a payment service user", func() { + var ( + psuID string + ) + + BeforeEach(func() { + createResponse, err := app.GetValue().SDK().Payments.V3.CreatePaymentServiceUser(ctx, v3CreateRequest) + Expect(err).To(BeNil()) + psuID = createResponse.GetV3CreatePaymentServiceUserResponse().Data + }) + + It("should be ok", func() { + id, err := uuid.Parse(psuID) + Expect(err).To(BeNil()) + + getResponse, err := app.GetValue().SDK().Payments.V3.GetPaymentServiceUser(ctx, psuID) + Expect(err).To(BeNil()) + Expect(getResponse.GetV3GetPaymentServiceUserResponse().Data.ID).To(Equal(id.String())) + }) + }) + + When("adding a bank account to a payment service user", func() { + var ( + psuID string + ) + + BeforeEach(func() { + createResponse, err := app.GetValue().SDK().Payments.V3.CreatePaymentServiceUser(ctx, v3CreateRequest) + Expect(err).To(BeNil()) + psuID = createResponse.GetV3CreatePaymentServiceUserResponse().Data + }) + + It("should be ok", func() { + _, err := app.GetValue().SDK().Payments.V3.AddBankAccountToPaymentServiceUser(ctx, psuID, baID2.String()) + Expect(err).To(BeNil()) + + getResponse, err := app.GetValue().SDK().Payments.V3.GetPaymentServiceUser(ctx, psuID) + Expect(err).To(BeNil()) + Expect(getResponse.GetV3GetPaymentServiceUserResponse().Data.BankAccountIDs).To(ContainElement(baID2.String())) + }) + + It("should not do anything if the bank account is already added", func() { + _, err := app.GetValue().SDK().Payments.V3.AddBankAccountToPaymentServiceUser(ctx, psuID, baID2.String()) + Expect(err).To(BeNil()) + + getResponse, err := app.GetValue().SDK().Payments.V3.GetPaymentServiceUser(ctx, psuID) + Expect(err).To(BeNil()) + Expect(getResponse.GetV3GetPaymentServiceUserResponse().Data.BankAccountIDs).To(ContainElement(baID2.String())) + + _, err = app.GetValue().SDK().Payments.V3.AddBankAccountToPaymentServiceUser(ctx, psuID, baID2.String()) + Expect(err).To(BeNil()) + + getResponse, err = app.GetValue().SDK().Payments.V3.GetPaymentServiceUser(ctx, psuID) + Expect(err).To(BeNil()) + Expect(getResponse.GetV3GetPaymentServiceUserResponse().Data.BankAccountIDs).To(ContainElement(baID2.String())) + }) + + It("should fail if bank account does not exists", func() { + _, err := app.GetValue().SDK().Payments.V3.AddBankAccountToPaymentServiceUser(ctx, psuID, uuid.New().String()) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("failed to add bank account to payment service user: bank_account_id: value not found")) + }) + + It("should fail if payment service user does not exists", func() { + _, err := app.GetValue().SDK().Payments.V3.AddBankAccountToPaymentServiceUser(ctx, uuid.New().String(), baID2.String()) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("failed to add bank account to payment service user: value not found")) + }) + }) + + When("forwarding a psu bank account to a connector", func() { + var ( + connectorID string + e chan *nats.Msg + psuID string + ) + + BeforeEach(func() { + e = Subscribe(GinkgoT(), app.GetValue()) + + createResponse, err := app.GetValue().SDK().Payments.V3.CreatePaymentServiceUser(ctx, v3CreateRequest) + Expect(err).To(BeNil()) + psuID = createResponse.GetV3CreatePaymentServiceUserResponse().Data + + connectorID, err = installConnector(ctx, app.GetValue(), uuid.New(), 3) + Expect(err).To(BeNil()) + }) + + AfterEach(func() { + uninstallConnector(ctx, app.GetValue(), connectorID) + }) + + It("should fail when connector ID is invalid", func() { + _, err := app.GetValue().SDK().Payments.V3.ForwardPaymentServiceUserBankAccount(ctx, psuID, baID1.String(), &components.V3ForwardPaymentServiceUserBankAccountRequest{ + ConnectorID: "invalid", + }) + Expect(err).NotTo(BeNil()) + Expect(err.Error()).To(ContainSubstring("400")) + }) + + It("should fail when bank account ID is invalid", func() { + _, err := app.GetValue().SDK().Payments.V3.ForwardPaymentServiceUserBankAccount(ctx, psuID, "invalid", &components.V3ForwardPaymentServiceUserBankAccountRequest{ + ConnectorID: connectorID, + }) + Expect(err).NotTo(BeNil()) + Expect(err.Error()).To(ContainSubstring("400")) + }) + + It("should be ok when connector is installed", func() { + forwardResponse, err := app.GetValue().SDK().Payments.V3.ForwardPaymentServiceUserBankAccount(ctx, psuID, baID1.String(), &components.V3ForwardPaymentServiceUserBankAccountRequest{ + ConnectorID: connectorID, + }) + Expect(err).To(BeNil()) + taskID, err := models.TaskIDFromString(forwardResponse.GetV3ForwardPaymentServiceUserBankAccountResponse().Data.TaskID) + Expect(err).To(BeNil()) + Expect(taskID.Reference).To(ContainSubstring(baID1.String())) + cID := models.MustConnectorIDFromString(connectorID) + Expect(taskID.Reference).To(ContainSubstring(cID.Reference.String())) + + connectorID, err := models.ConnectorIDFromString(connectorID) + Expect(err).To(BeNil()) + + getResponse, err := app.GetValue().SDK().Payments.V3.GetBankAccount(ctx, baID1.String()) + Expect(err).To(BeNil()) + + Expect(getResponse.GetV3GetBankAccountResponse().Data.AccountNumber).ToNot(BeNil()) + accountNumber := *getResponse.GetV3GetBankAccountResponse().Data.AccountNumber + Expect(getResponse.GetV3GetBankAccountResponse().Data.Iban).ToNot(BeNil()) + iban := *getResponse.GetV3GetBankAccountResponse().Data.Iban + + accountID := models.AccountID{ + Reference: fmt.Sprintf("dummypay-%s", baID1.String()), + ConnectorID: connectorID, + } + + Eventually(e).Should(Receive(Event(evts.EventTypeSavedBankAccount, WithPayload( + events.BankAccountMessagePayload{ + ID: baID1.String(), + Country: "DE", + Name: getResponse.GetV3GetBankAccountResponse().Data.Name, + AccountNumber: fmt.Sprintf("%s****%s", accountNumber[0:2], accountNumber[len(accountNumber)-3:]), + IBAN: fmt.Sprintf("%s**************%s", iban[0:4], iban[len(iban)-4:]), + CreatedAt: getResponse.GetV3GetBankAccountResponse().Data.GetCreatedAt(), + Metadata: map[string]string{ + "com.formance.spec/owner/addressLine1": "1 test", + "com.formance.spec/owner/city": "test", + "com.formance.spec/owner/email": "test", + "com.formance.spec/owner/postalCode": "test", + "com.formance.spec/owner/region": "test", + "com.formance.spec/owner/streetName": "test", + "com.formance.spec/owner/streetNumber": "1", + }, + RelatedAccounts: []events.BankAccountRelatedAccountsPayload{ + { + AccountID: accountID.String(), + CreatedAt: getResponse.GetV3GetBankAccountResponse().Data.GetCreatedAt(), + ConnectorID: connectorID.String(), + Provider: "dummypay", + }, + }, + }, + )))) + }) + }) +}) From 5619a7393cb24730106bfdbac0d6f8206f53b3e1 Mon Sep 17 00:00:00 2001 From: Paul Nicolas Date: Fri, 25 Apr 2025 13:33:37 +0200 Subject: [PATCH 06/13] fix go lint tests --- internal/storage/psu_test.go | 24 ++++++++++-------------- 1 file changed, 10 insertions(+), 14 deletions(-) diff --git a/internal/storage/psu_test.go b/internal/storage/psu_test.go index 7260078d1..0fdb3a59a 100644 --- a/internal/storage/psu_test.go +++ b/internal/storage/psu_test.go @@ -369,17 +369,15 @@ func compareCounterPartiesAddressed(t *testing.T, expected, actual *models.Addre case expected == nil && actual == nil: return case expected != nil && actual != nil: - // Do the next tests + compareInterface(t, "StreetName", expected.StreetName, actual.StreetName) + compareInterface(t, "StreetNumber", expected.StreetNumber, actual.StreetNumber) + compareInterface(t, "City", expected.City, actual.City) + compareInterface(t, "Region", expected.Region, actual.Region) + compareInterface(t, "PostalCode", expected.PostalCode, actual.PostalCode) + compareInterface(t, "Country", expected.Country, actual.Country) default: require.Fail(t, "Address is different") } - - compareInterface(t, "StreetName", expected.StreetName, actual.StreetName) - compareInterface(t, "StreetNumber", expected.StreetNumber, actual.StreetNumber) - compareInterface(t, "City", expected.City, actual.City) - compareInterface(t, "Region", expected.Region, actual.Region) - compareInterface(t, "PostalCode", expected.PostalCode, actual.PostalCode) - compareInterface(t, "Country", expected.Country, actual.Country) } func compareCounterPartiesContactDetails(t *testing.T, expected, actual *models.ContactDetails) { @@ -387,24 +385,22 @@ func compareCounterPartiesContactDetails(t *testing.T, expected, actual *models. case expected == nil && actual == nil: return case expected != nil && actual != nil: - // Do the next tests + compareInterface(t, "Email", expected.Email, actual.Email) + compareInterface(t, "Phone", expected.PhoneNumber, actual.PhoneNumber) default: require.Fail(t, "ContactDetails is different") } - compareInterface(t, "Email", expected.Email, actual.Email) - compareInterface(t, "Phone", expected.PhoneNumber, actual.PhoneNumber) } -func compareInterface(t *testing.T, name string, expected, actual interface{}) { +func compareInterface(t *testing.T, name string, expected, actual *string) { switch { case expected == nil && actual == nil: return case expected != nil && actual != nil: - // Do the next tests + require.Equal(t, expected, actual) default: require.Failf(t, "%s field is different", name) } - require.Equal(t, expected, actual) } From f2bbef12724471d975324e3d03c6181bed34c188 Mon Sep 17 00:00:00 2001 From: Paul Nicolas Date: Fri, 25 Apr 2025 13:46:14 +0200 Subject: [PATCH 07/13] fixes after coderabbit review --- docs/api/README.md | 8 ++++---- internal/api/services/payment_service_users_get_test.go | 4 ++-- internal/storage/payment_initiations.go | 2 +- internal/storage/psu.go | 2 +- openapi.yaml | 2 +- openapi/v3/v3-schemas.yaml | 2 +- pkg/client/.speakeasy/gen.lock | 2 +- .../docs/models/components/v3contactdetailsrequest.md | 2 +- pkg/client/models/components/v3contactdetailsrequest.go | 8 ++++---- test/e2e/api_payment_service_users_test.go | 4 ++-- 10 files changed, 18 insertions(+), 18 deletions(-) diff --git a/docs/api/README.md b/docs/api/README.md index 14f9dfcec..02e19a311 100644 --- a/docs/api/README.md +++ b/docs/api/README.md @@ -2205,7 +2205,7 @@ Accept: application/json "name": "string", "contactDetails": { "email": "string", - "phoneNuber": "string" + "phoneNumber": "string" }, "address": { "streetNumber": "string", @@ -4909,7 +4909,7 @@ None ( Scopes: payments:read ) "name": "string", "contactDetails": { "email": "string", - "phoneNuber": "string" + "phoneNumber": "string" }, "address": { "streetNumber": "string", @@ -4980,7 +4980,7 @@ None ( Scopes: payments:read ) ```json { "email": "string", - "phoneNuber": "string" + "phoneNumber": "string" } ``` @@ -4990,7 +4990,7 @@ None ( Scopes: payments:read ) |Name|Type|Required|Restrictions|Description| |---|---|---|---|---| |email|string|false|none|none| -|phoneNuber|string|false|none|none| +|phoneNumber|string|false|none|none|

V3CreatePaymentServiceUserResponse

diff --git a/internal/api/services/payment_service_users_get_test.go b/internal/api/services/payment_service_users_get_test.go index 5f09ac5dd..829a659ab 100644 --- a/internal/api/services/payment_service_users_get_test.go +++ b/internal/api/services/payment_service_users_get_test.go @@ -50,9 +50,9 @@ func TestPSUGet(t *testing.T) { for _, test := range tests { t.Run(test.name, func(t *testing.T) { store.EXPECT().PaymentServiceUsersGet(gomock.Any(), id).Return(&models.PaymentServiceUser{}, test.err) - bankAccount, err := s.PaymentServiceUsersGet(context.Background(), id) + psu, err := s.PaymentServiceUsersGet(context.Background(), id) if test.expectedError == nil { - require.NotNil(t, bankAccount) + require.NotNil(t, psu) require.NoError(t, err) } else { require.Equal(t, test.expectedError, err) diff --git a/internal/storage/payment_initiations.go b/internal/storage/payment_initiations.go index ede5d2230..361d3854a 100644 --- a/internal/storage/payment_initiations.go +++ b/internal/storage/payment_initiations.go @@ -442,7 +442,7 @@ func (s *store) paymentsInitiationAdjustmentsQueryContext(qb query.Builder) (str switch { case key == "status": if operator != "$match" { - return "", nil, fmt.Errorf("'type' column can only be used with $match: %w", ErrValidation) + return "", nil, fmt.Errorf("'status' column can only be used with $match: %w", ErrValidation) } return fmt.Sprintf("%s = ?", key), []any{value}, nil case metadataRegex.Match([]byte(key)): diff --git a/internal/storage/psu.go b/internal/storage/psu.go index 9943977a0..6d6f5f755 100644 --- a/internal/storage/psu.go +++ b/internal/storage/psu.go @@ -101,7 +101,7 @@ func (s *store) PaymentServiceUsersCreate(ctx context.Context, psu models.Paymen if err := tx.Commit(); err != nil { errTx = err - return e("commit transaction", tx.Commit()) + return e("commit transaction", err) } return nil diff --git a/openapi.yaml b/openapi.yaml index 7d570e0c5..679773e05 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -4774,7 +4774,7 @@ components: properties: email: type: string - phoneNuber: + phoneNumber: type: string V3CreatePaymentServiceUserResponse: type: object diff --git a/openapi/v3/v3-schemas.yaml b/openapi/v3/v3-schemas.yaml index de51d1823..b13626b89 100644 --- a/openapi/v3/v3-schemas.yaml +++ b/openapi/v3/v3-schemas.yaml @@ -1192,7 +1192,7 @@ components: properties: email: type: string - phoneNuber: + phoneNumber: type: string V3CreatePaymentServiceUserResponse: diff --git a/pkg/client/.speakeasy/gen.lock b/pkg/client/.speakeasy/gen.lock index 7d39d4fa0..4b6dc3b6a 100644 --- a/pkg/client/.speakeasy/gen.lock +++ b/pkg/client/.speakeasy/gen.lock @@ -1,7 +1,7 @@ lockVersion: 2.0.0 id: 86668c33-fde8-4164-be98-cfc24d333a1e management: - docChecksum: 32653a9c28a615b4231dae33001bd5ae + docChecksum: ae0a09aa698536274b47d1d29a0c8ad2 docVersion: v1 speakeasyVersion: 1.525.0 generationVersion: 2.562.2 diff --git a/pkg/client/docs/models/components/v3contactdetailsrequest.md b/pkg/client/docs/models/components/v3contactdetailsrequest.md index 7d4a32b81..6caf7fc66 100644 --- a/pkg/client/docs/models/components/v3contactdetailsrequest.md +++ b/pkg/client/docs/models/components/v3contactdetailsrequest.md @@ -6,4 +6,4 @@ | Field | Type | Required | Description | | ------------------ | ------------------ | ------------------ | ------------------ | | `Email` | **string* | :heavy_minus_sign: | N/A | -| `PhoneNuber` | **string* | :heavy_minus_sign: | N/A | \ No newline at end of file +| `PhoneNumber` | **string* | :heavy_minus_sign: | N/A | \ No newline at end of file diff --git a/pkg/client/models/components/v3contactdetailsrequest.go b/pkg/client/models/components/v3contactdetailsrequest.go index 1b2c8c685..06401086d 100644 --- a/pkg/client/models/components/v3contactdetailsrequest.go +++ b/pkg/client/models/components/v3contactdetailsrequest.go @@ -3,8 +3,8 @@ package components type V3ContactDetailsRequest struct { - Email *string `json:"email,omitempty"` - PhoneNuber *string `json:"phoneNuber,omitempty"` + Email *string `json:"email,omitempty"` + PhoneNumber *string `json:"phoneNumber,omitempty"` } func (o *V3ContactDetailsRequest) GetEmail() *string { @@ -14,9 +14,9 @@ func (o *V3ContactDetailsRequest) GetEmail() *string { return o.Email } -func (o *V3ContactDetailsRequest) GetPhoneNuber() *string { +func (o *V3ContactDetailsRequest) GetPhoneNumber() *string { if o == nil { return nil } - return o.PhoneNuber + return o.PhoneNumber } diff --git a/test/e2e/api_payment_service_users_test.go b/test/e2e/api_payment_service_users_test.go index 5e1912cee..9add93e4f 100644 --- a/test/e2e/api_payment_service_users_test.go +++ b/test/e2e/api_payment_service_users_test.go @@ -45,8 +45,8 @@ var _ = Context("Payment API Payment Service Users", func() { v3CreateRequest = &components.V3CreatePaymentServiceUserRequest{ Name: "test", ContactDetails: &components.V3ContactDetailsRequest{ - Email: pointer.For("test"), - PhoneNuber: pointer.For("test"), + Email: pointer.For("test"), + PhoneNumber: pointer.For("test"), }, Address: &components.V3AddressRequest{ StreetNumber: pointer.For("1"), From a935476f6161a7c1313440c374dfc1e0d3e75f7a Mon Sep 17 00:00:00 2001 From: Paul Nicolas Date: Fri, 25 Apr 2025 13:52:20 +0200 Subject: [PATCH 08/13] fix typos --- internal/api/services/bank_accounts_forward_to_connector.go | 2 +- .../services/payment_service_users_forward_bank_account.go | 2 +- internal/models/psu.go | 6 +++++- internal/storage/psu_test.go | 6 +++--- 4 files changed, 10 insertions(+), 6 deletions(-) diff --git a/internal/api/services/bank_accounts_forward_to_connector.go b/internal/api/services/bank_accounts_forward_to_connector.go index 4e308e892..63bbb821b 100644 --- a/internal/api/services/bank_accounts_forward_to_connector.go +++ b/internal/api/services/bank_accounts_forward_to_connector.go @@ -14,7 +14,7 @@ func (s *Service) BankAccountsForwardToConnector(ctx context.Context, bankAccoun } if ba == nil { - // Should not happened, but just in case + // Should not happen, but just in case return models.Task{}, newStorageError(nil, "bank account not found") } diff --git a/internal/api/services/payment_service_users_forward_bank_account.go b/internal/api/services/payment_service_users_forward_bank_account.go index c5e2aa1c0..1cc5f673e 100644 --- a/internal/api/services/payment_service_users_forward_bank_account.go +++ b/internal/api/services/payment_service_users_forward_bank_account.go @@ -15,7 +15,7 @@ func (s *Service) PaymentServiceUsersForwardBankAccountToConnector(ctx context.C } if ba == nil { - // Should not happened, but just in case + // Should not happen, but just in case return models.Task{}, newStorageError(storage.ErrNotFound, "bank account not found") } diff --git a/internal/models/psu.go b/internal/models/psu.go index ed4505794..bd32e00ec 100644 --- a/internal/models/psu.go +++ b/internal/models/psu.go @@ -77,7 +77,11 @@ func (psu *PaymentServiceUser) UnmarshalJSON(data []byte) error { return err } - psu.ID, _ = uuid.Parse(aux.ID) + var err error + psu.ID, err = uuid.Parse(aux.ID) + if err != nil { + return err + } psu.Name = aux.Name psu.CreatedAt = aux.CreatedAt psu.ContactDetails = aux.ContactDetails diff --git a/internal/storage/psu_test.go b/internal/storage/psu_test.go index 0fdb3a59a..6cea83c3f 100644 --- a/internal/storage/psu_test.go +++ b/internal/storage/psu_test.go @@ -203,7 +203,7 @@ func TestPSUList(t *testing.T) { require.Empty(t, cursor.Next) }) - t.Run("lsit psu by metadata", func(t *testing.T) { + t.Run("list psu by metadata", func(t *testing.T) { q := NewListPSUQuery( bunpaginate.NewPaginatedQueryOptions(PSUQuery{}). WithPageSize(15). @@ -219,7 +219,7 @@ func TestPSUList(t *testing.T) { comparePSUs(t, defaultPSU, cursor.Data[0]) }) - t.Run("lsit psu by unknown metadata", func(t *testing.T) { + t.Run("list psu by unknown metadata", func(t *testing.T) { q := NewListPSUQuery( bunpaginate.NewPaginatedQueryOptions(PSUQuery{}). WithPageSize(15). @@ -234,7 +234,7 @@ func TestPSUList(t *testing.T) { require.Empty(t, cursor.Next) }) - t.Run("lsit psu by metadata", func(t *testing.T) { + t.Run("list psu by metadata", func(t *testing.T) { q := NewListPSUQuery( bunpaginate.NewPaginatedQueryOptions(PSUQuery{}). WithPageSize(15). From 57534cda026e18a20ee304a2307e8bc8a40af795 Mon Sep 17 00:00:00 2001 From: Paul Nicolas Date: Fri, 25 Apr 2025 14:07:02 +0200 Subject: [PATCH 09/13] fix tests --- test/e2e/api_payment_service_users_test.go | 1 + 1 file changed, 1 insertion(+) diff --git a/test/e2e/api_payment_service_users_test.go b/test/e2e/api_payment_service_users_test.go index 9add93e4f..8f208cae1 100644 --- a/test/e2e/api_payment_service_users_test.go +++ b/test/e2e/api_payment_service_users_test.go @@ -234,6 +234,7 @@ var _ = Context("Payment API Payment Service Users", func() { "com.formance.spec/owner/addressLine1": "1 test", "com.formance.spec/owner/city": "test", "com.formance.spec/owner/email": "test", + "com.formance.spec/owner/phoneNumber": "test", "com.formance.spec/owner/postalCode": "test", "com.formance.spec/owner/region": "test", "com.formance.spec/owner/streetName": "test", From 1d0691906447e51b344d572db9e6a71e0a89aa24 Mon Sep 17 00:00:00 2001 From: Paul Nicolas Date: Mon, 28 Apr 2025 10:57:39 +0200 Subject: [PATCH 10/13] feat(validation): add validation on email and phone number --- .../handler_payment_service_users_create.go | 6 +- ...ndler_payment_service_users_create_test.go | 8 ++- internal/api/validation/checkers.go | 25 +++++++ internal/api/validation/validation.go | 2 + internal/api/validation/validation_test.go | 70 +++++++++++++++++++ 5 files changed, 105 insertions(+), 6 deletions(-) diff --git a/internal/api/v3/handler_payment_service_users_create.go b/internal/api/v3/handler_payment_service_users_create.go index bd9199c4b..c8c94467b 100644 --- a/internal/api/v3/handler_payment_service_users_create.go +++ b/internal/api/v3/handler_payment_service_users_create.go @@ -17,13 +17,13 @@ import ( ) type ContactDetailsRequest struct { - Email *string `json:"email,omitempty"` - PhoneNumber *string `json:"phoneNumber,omitempty"` + Email *string `json:"email,omitempty" validate:"omitempty,email"` + PhoneNumber *string `json:"phoneNumber,omitempty" validate:"omitempty,phoneNumber"` } type AddressRequest struct { StreetName *string `json:"streetName,omitempty"` - StreetNumber *string `json:"streetNumber,omitempty" validate:"omitempty,number"` + StreetNumber *string `json:"streetNumber,omitempty" validate:"omitempty,alphanum"` City *string `json:"city,omitempty"` Region *string `json:"region,omitempty"` PostalCode *string `json:"postalCode,omitempty"` diff --git a/internal/api/v3/handler_payment_service_users_create_test.go b/internal/api/v3/handler_payment_service_users_create_test.go index 3caf6ae43..edc7cab6c 100644 --- a/internal/api/v3/handler_payment_service_users_create_test.go +++ b/internal/api/v3/handler_payment_service_users_create_test.go @@ -49,7 +49,9 @@ var _ = Describe("API v3 Payment Service Users Create", func() { Entry("name missing", PaymentServiceUsersCreateRequest{}), Entry("name too long", PaymentServiceUsersCreateRequest{Name: generateTextString(1001)}), Entry("country invalid", PaymentServiceUsersCreateRequest{Name: "a", Address: &AddressRequest{Country: pointer.For("invalid")}}), - Entry("street number invalid", PaymentServiceUsersCreateRequest{Name: "a", Address: &AddressRequest{StreetNumber: pointer.For("invalid")}}), + Entry("phone number invalid", PaymentServiceUsersCreateRequest{Name: "a", ContactDetails: &ContactDetailsRequest{PhoneNumber: pointer.For("invalid")}}), + Entry("email invalid", PaymentServiceUsersCreateRequest{Name: "a", ContactDetails: &ContactDetailsRequest{Email: pointer.For("invalid")}}), + Entry("street number invalid", PaymentServiceUsersCreateRequest{Name: "a", Address: &AddressRequest{StreetNumber: pointer.For("invalid@")}}), ) It("should return an internal server error when backend returns error", func(ctx SpecContext) { @@ -78,8 +80,8 @@ var _ = Describe("API v3 Payment Service Users Create", func() { psuReq := PaymentServiceUsersCreateRequest{ Name: "reference", ContactDetails: &ContactDetailsRequest{ - Email: pointer.For("test"), - PhoneNumber: pointer.For("test"), + Email: pointer.For("test@formance.com"), + PhoneNumber: pointer.For("+3312131415"), }, Address: &AddressRequest{ StreetName: pointer.For("test"), diff --git a/internal/api/validation/checkers.go b/internal/api/validation/checkers.go index 1e22d3c41..8494a0a29 100644 --- a/internal/api/validation/checkers.go +++ b/internal/api/validation/checkers.go @@ -1,12 +1,19 @@ package validation import ( + "regexp" + "github.com/formancehq/payments/internal/connectors/plugins/currency" "github.com/formancehq/payments/internal/models" ut "github.com/go-playground/universal-translator" "github.com/go-playground/validator/v10" ) +var ( + phoneNumberRegexp = regexp.MustCompile(`^\+?\d{1,4}?[-.\s]?\(?\d{1,3}?\)?[-.\s]?\d{1,4}[-.\s]?\d{1,4}[-.\s]?\d{1,9}$`) + emailRegexp = regexp.MustCompile(`^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$`) +) + //nolint:errcheck func registerCustomChecker( tagName string, @@ -138,3 +145,21 @@ func IsAsset(fl validator.FieldLevel) bool { } return true } + +func IsPhoneNumber(fl validator.FieldLevel) bool { + str, err := fieldLevelToString(fl) + if err != nil { + return false + } + + return phoneNumberRegexp.MatchString(str) +} + +func IsEmail(fl validator.FieldLevel) bool { + str, err := fieldLevelToString(fl) + if err != nil { + return false + } + + return emailRegexp.MatchString(str) +} diff --git a/internal/api/validation/validation.go b/internal/api/validation/validation.go index a8bb77083..82dded956 100644 --- a/internal/api/validation/validation.go +++ b/internal/api/validation/validation.go @@ -42,6 +42,8 @@ func NewValidator() *Validator { registerCustomChecker("paymentStatus", IsPaymentStatus, "", validate, translator) registerCustomChecker("paymentInitiationType", IsPaymentInitiationType, "", validate, translator) registerCustomChecker("asset", IsAsset, "", validate, translator) + registerCustomChecker("phoneNumber", IsPhoneNumber, "", validate, translator) + registerCustomChecker("email", IsEmail, "", validate, translator) return &Validator{ internal: validate, translator: translator, diff --git a/internal/api/validation/validation_test.go b/internal/api/validation/validation_test.go index dfecc2d1e..37ae9dbbe 100644 --- a/internal/api/validation/validation_test.go +++ b/internal/api/validation/validation_test.go @@ -40,6 +40,10 @@ var _ = Describe("Validator custom type checks", func() { PaymentInitiationTypeStr string `validate:"omitempty,paymentInitiationType"` Asset string `validate:"omitempty,asset"` AssetNullable *string `validate:"omitempty,asset"` + PhoneNumber string `validate:"omitempty,phoneNumber"` + PhoneNumberNullable *string `validate:"omitempty,phoneNumber"` + Email string `validate:"omitempty,email"` + EmailNullable *string `validate:"omitempty,email"` } DescribeTable("non conforming values", @@ -170,6 +174,40 @@ var _ = Describe("Validator custom type checks", func() { Entry("asset: unsupported type for this matcher", "asset", "FieldName", struct { FieldName int `validate:"asset"` }{FieldName: 34}), + + // phoneNumber + Entry("phoneNumber: invalid value of string on required field", "phoneNumber", "StringFieldName", struct { + StringFieldName string `validate:"required,phoneNumber"` + }{StringFieldName: "invalid"}), + Entry("phoneNumber: invalid value of string", "phoneNumber", "StringFieldName", struct { + StringFieldName string `validate:"omitempty,phoneNumber"` + }{StringFieldName: "invalid"}), + Entry("phoneNumber: invalid value of string on required field", "phoneNumber", "StringFieldName", struct { + StringFieldName *string `validate:"required,phoneNumber"` + }{StringFieldName: pointer.For("invalid")}), + Entry("phoneNumber: invalid value of string", "phoneNumber", "StringFieldName", struct { + StringFieldName *string `validate:"omitempty,phoneNumber"` + }{StringFieldName: pointer.For("invalid")}), + Entry("phoneNumber: unsupported type for this matcher", "phoneNumber", "FieldName", struct { + FieldName int `validate:"phoneNumber"` + }{FieldName: 34}), + + // email + Entry("email: invalid value of string on required field", "email", "StringFieldName", struct { + StringFieldName string `validate:"required,email"` + }{StringFieldName: "invalid"}), + Entry("email: invalid value of string", "email", "StringFieldName", struct { + StringFieldName string `validate:"omitempty,email"` + }{StringFieldName: "invalid"}), + Entry("email: invalid value of string on required field", "email", "StringFieldName", struct { + StringFieldName *string `validate:"required,email"` + }{StringFieldName: pointer.For("invalid")}), + Entry("email: invalid value of string", "email", "StringFieldName", struct { + StringFieldName *string `validate:"omitempty,email"` + }{StringFieldName: pointer.For("invalid")}), + Entry("email: unsupported type for this matcher", "email", "FieldName", struct { + FieldName int `validate:"email"` + }{FieldName: 34}), ) It("connectorID supports expected values", func(ctx SpecContext) { @@ -223,5 +261,37 @@ var _ = Describe("Validator custom type checks", func() { }) Expect(err).To(BeNil()) }) + It("phoneNumber supports expected values", func(ctx SpecContext) { + _, err := validate.Validate(CustomStruct{ + PhoneNumber: "+330612131415", + PhoneNumberNullable: pointer.For("+330612131415"), + }) + Expect(err).To(BeNil()) + + _, err = validate.Validate(CustomStruct{ + PhoneNumber: "0612131415", + PhoneNumberNullable: pointer.For("0612131415"), + }) + Expect(err).To(BeNil()) + + _, err = validate.Validate(CustomStruct{ + PhoneNumber: "+1 (555) 555-1234", + PhoneNumberNullable: pointer.For("+1 (555) 555-1234"), + }) + Expect(err).To(BeNil()) + + _, err = validate.Validate(CustomStruct{ + PhoneNumber: "00 1 202 555 0123", + PhoneNumberNullable: pointer.For("00 1 202 555 0123"), + }) + Expect(err).To(BeNil()) + }) + It("email supports expected values", func(ctx SpecContext) { + _, err := validate.Validate(CustomStruct{ + Email: "dev@formance.com", + EmailNullable: pointer.For("dev@formance.com"), + }) + Expect(err).To(BeNil()) + }) }) }) From b756de3b54685d48a56fc70c5d862ec36b50884e Mon Sep 17 00:00:00 2001 From: Paul Nicolas Date: Mon, 28 Apr 2025 16:16:17 +0200 Subject: [PATCH 11/13] use classic one to many postgres relationship --- internal/storage/bank_accounts.go | 20 +++-- .../migrations/14-create-psu-tables.sql | 20 ++--- internal/storage/psu.go | 89 +++++++++---------- internal/storage/psu_test.go | 10 +-- test/e2e/api_payment_service_users_test.go | 10 +-- 5 files changed, 73 insertions(+), 76 deletions(-) diff --git a/internal/storage/bank_accounts.go b/internal/storage/bank_accounts.go index 364442148..146d59cbf 100644 --- a/internal/storage/bank_accounts.go +++ b/internal/storage/bank_accounts.go @@ -34,7 +34,10 @@ type bankAccount struct { // c.f. https://bun.uptrace.dev/guide/models.html#default Metadata map[string]string `bun:"metadata,type:jsonb,nullzero,notnull,default:'{}'"` - RelatedAccounts []*bankAccountRelatedAccount `bun:"rel:has-many,join:id=bank_account_id"` + PsuID *uuid.UUID `bun:"psu_id,type:uuid,nullzero"` + + PSU *paymentServiceUser `bun:"rel:belongs-to,join:psu_id=id,scanonly"` + RelatedAccounts []*bankAccountRelatedAccount `bun:"rel:has-many,join:id=bank_account_id,scanonly"` } func (s *store) BankAccountsUpsert(ctx context.Context, ba models.BankAccount) error { @@ -42,24 +45,28 @@ func (s *store) BankAccountsUpsert(ctx context.Context, ba models.BankAccount) e if err != nil { return e("begin transaction", err) } + + var errTx error defer func() { - rollbackOnTxError(ctx, &tx, err) + rollbackOnTxError(ctx, &tx, errTx) }() toInsert := fromBankAccountModels(ba) // Insert or update the bank account res, err := tx.NewInsert(). Model(&toInsert). - Column("id", "created_at", "name", "country", "metadata"). + Column("id", "created_at", "name", "country", "metadata", "psu_id"). On("CONFLICT (id) DO NOTHING"). Returning("id"). Exec(ctx) if err != nil { + errTx = err return e("insert bank account", err) } rowsAffected, err := res.RowsAffected() if err != nil { + errTx = err return e("insert bank account", err) } @@ -72,6 +79,7 @@ func (s *store) BankAccountsUpsert(ctx context.Context, ba models.BankAccount) e Where("id = ?", toInsert.ID). Exec(ctx) if err != nil { + errTx = err return e("update bank account", err) } } @@ -83,6 +91,7 @@ func (s *store) BankAccountsUpsert(ctx context.Context, ba models.BankAccount) e On("CONFLICT (bank_account_id, account_id) DO NOTHING"). Exec(ctx) if err != nil { + errTx = err return e("insert related accounts", err) } } @@ -133,7 +142,7 @@ func (s *store) BankAccountsGet(ctx context.Context, id uuid.UUID, expand bool) var account bankAccount query := s.db.NewSelect(). Model(&account). - Column("id", "created_at", "name", "country", "metadata"). + Column("id", "created_at", "name", "country", "metadata", "psu_id"). Relation("RelatedAccounts") if expand { query = query.ColumnExpr("pgp_sym_decrypt(account_number, ?, ?) AS decrypted_account_number", s.configEncryptionKey, encryptionOptions). @@ -163,7 +172,7 @@ func NewListBankAccountsQuery(opts bunpaginate.PaginatedQueryOptions[BankAccount func (s *store) bankAccountsQueryContext(qb query.Builder) (string, []any, error) { return qb.Build(query.ContextFn(func(key, operator string, value any) (string, []any, error) { switch { - case key == "name", key == "country", key == "id": + case key == "name", key == "country", key == "id", key == "psu_id": if operator != "$match" { return "", nil, fmt.Errorf("'%s' column can only be used with $match: %w", key, ErrValidation) } @@ -271,6 +280,7 @@ func fromBankAccountModels(from models.BankAccount) bankAccount { Name: from.Name, Country: from.Country, Metadata: from.Metadata, + PsuID: nil, } if from.AccountNumber != nil { diff --git a/internal/storage/migrations/14-create-psu-tables.sql b/internal/storage/migrations/14-create-psu-tables.sql index 02c4d600f..fce108114 100644 --- a/internal/storage/migrations/14-create-psu-tables.sql +++ b/internal/storage/migrations/14-create-psu-tables.sql @@ -27,21 +27,11 @@ create table if not exists payment_service_users ( create index psu_created_at_sort_id on payment_service_users (created_at, sort_id); -create table if not exists psu_bank_accounts ( - -- Mandatory fields - psu_id uuid not null, - bank_account_id uuid not null, - - primary key (psu_id, bank_account_id) -); +alter table bank_accounts + add column if not exists psu_id uuid default null; -alter table psu_bank_accounts - add constraint fk_psu_bank_accounts_psu_id +alter table bank_accounts + add constraint fk_psu_id foreign key (psu_id) references payment_service_users (id) - on delete cascade; -alter table psu_bank_accounts - add constraint fk_psu_bank_accounts_bank_account_id - foreign key (bank_account_id) - references bank_accounts (id) - on delete cascade; \ No newline at end of file + on delete set null; \ No newline at end of file diff --git a/internal/storage/psu.go b/internal/storage/psu.go index 6d6f5f755..58895b6a6 100644 --- a/internal/storage/psu.go +++ b/internal/storage/psu.go @@ -34,15 +34,7 @@ type paymentServiceUser struct { Metadata map[string]string `bun:"metadata,type:jsonb,nullzero,notnull,default:'{}'"` // Relations - RelatedBankAccounts []psuBankAccounts `bun:"rel:has-many,join:id=psu_id"` -} - -type psuBankAccounts struct { - bun.BaseModel `bun:"psu_bank_accounts"` - - // Mandatory fields - PsuID uuid.UUID `bun:"psu_id,pk,type:uuid,notnull"` - BankAccountID uuid.UUID `bun:"bank_account_id,pk,type:uuid,notnull"` + BankAccounts []bankAccount `bun:"rel:has-many,join:id=psu_id"` } func (s *store) PaymentServiceUsersCreate(ctx context.Context, psu models.PaymentServiceUser) error { @@ -55,11 +47,7 @@ func (s *store) PaymentServiceUsersCreate(ctx context.Context, psu models.Paymen var errTx error defer func() { - if errTx != nil { - if err := tx.Rollback(); err != nil { - s.logger.Errorf("failed to rollback transaction: %v", err) - } - } + rollbackOnTxError(ctx, &tx, errTx) }() _, err = tx.NewRaw(` @@ -88,14 +76,28 @@ func (s *store) PaymentServiceUsersCreate(ctx context.Context, psu models.Paymen } if len(relatedBankAccounts) > 0 { - // Insert or update the related accounts - _, err = tx.NewInsert(). - Model(&relatedBankAccounts). - On("CONFLICT (psu_id, bank_account_id) DO NOTHING"). - Exec(ctx) - if err != nil { - errTx = err - return e("insert related bank accounts", err) + // Update related bank accounts + for _, bankAccountID := range relatedBankAccounts { + res, err := tx.NewUpdate(). + Model((*bankAccount)(nil)). + Set("psu_id = ?", paymentServiceUser.ID). + Where("id = ?", bankAccountID). + Exec(ctx) + if err != nil { + errTx = err + return e("update bank account to add psu id", err) + } + + rowsAffected, err := res.RowsAffected() + if err != nil { + errTx = err + return e("update bank account to add psu id", err) + } + + if rowsAffected == 0 { + errTx = ErrNotFound + return e("bank account", ErrNotFound) + } } } @@ -113,7 +115,7 @@ func (s *store) PaymentServiceUsersGet(ctx context.Context, id uuid.UUID) (*mode Model(&psu). Column("id", "created_at", "metadata"). Where("id = ?", id). - Relation("RelatedBankAccounts") + Relation("BankAccounts") query = s.paymentServiceUsersSelectDecryptColumnExpr(query) @@ -180,7 +182,7 @@ func (s *store) PaymentServiceUsersList(ctx context.Context, query ListPSUsQuery cursor, err := paginateWithOffset[bunpaginate.PaginatedQueryOptions[PSUQuery], paymentServiceUser](s, ctx, (*bunpaginate.OffsetPaginatedQuery[bunpaginate.PaginatedQueryOptions[PSUQuery]])(&query), func(query *bun.SelectQuery) *bun.SelectQuery { - query = query.Relation("RelatedBankAccounts") + query = query.Relation("BankAccounts") query = query.Column("id", "created_at", "metadata") query = s.paymentServiceUsersSelectDecryptColumnExpr(query) @@ -212,17 +214,22 @@ func (s *store) PaymentServiceUsersList(ctx context.Context, query ListPSUsQuery } func (s *store) PaymentServiceUsersAddBankAccount(ctx context.Context, psuID, bankAccountID uuid.UUID) error { - toInsert := psuBankAccounts{ - PsuID: psuID, - BankAccountID: bankAccountID, + res, err := s.db.NewUpdate(). + Model((*bankAccount)(nil)). + Set("psu_id = ?", psuID). + Where("id = ?", bankAccountID). + Exec(ctx) + if err != nil { + return e("update bank account to add psu id", err) } - _, err := s.db.NewInsert(). - Model(&toInsert). - On("CONFLICT (psu_id, bank_account_id) DO NOTHING"). - Exec(ctx) + rowsAffected, err := res.RowsAffected() if err != nil { - return e("insert related bank account: %w", err) + return e("update bank account to add psu id", err) + } + + if rowsAffected == 0 { + return e("bank account", ErrNotFound) } return nil @@ -241,7 +248,7 @@ func (s *store) paymentServiceUsersSelectDecryptColumnExpr(query *bun.SelectQuer ColumnExpr("pgp_sym_decrypt(phone_number, ?, ?) as decrypted_phone", s.configEncryptionKey, encryptionOptions) } -func fromPaymentServiceUserModels(from models.PaymentServiceUser) (paymentServiceUser, []psuBankAccounts) { +func fromPaymentServiceUserModels(from models.PaymentServiceUser) (paymentServiceUser, []uuid.UUID) { psu := paymentServiceUser{ ID: from.ID, CreatedAt: time.New(from.CreatedAt), @@ -249,14 +256,6 @@ func fromPaymentServiceUserModels(from models.PaymentServiceUser) (paymentServic Metadata: from.Metadata, } - bankAccounts := make([]psuBankAccounts, len(from.BankAccountIDs)) - for i, id := range from.BankAccountIDs { - bankAccounts[i] = psuBankAccounts{ - PsuID: from.ID, - BankAccountID: id, - } - } - if from.Address != nil { psu.StreetName = from.Address.StreetName psu.StreetNumber = from.Address.StreetNumber @@ -271,7 +270,7 @@ func fromPaymentServiceUserModels(from models.PaymentServiceUser) (paymentServic psu.PhoneNumber = from.ContactDetails.PhoneNumber } - return psu, bankAccounts + return psu, from.BankAccountIDs } func toPaymentServiceUserModels(from paymentServiceUser) models.PaymentServiceUser { @@ -285,9 +284,9 @@ func toPaymentServiceUserModels(from paymentServiceUser) models.PaymentServiceUs psu.Address = fillAddress(from) psu.ContactDetails = fillContactDetails(from) - psu.BankAccountIDs = make([]uuid.UUID, len(from.RelatedBankAccounts)) - for i, bankAccount := range from.RelatedBankAccounts { - psu.BankAccountIDs[i] = bankAccount.BankAccountID + psu.BankAccountIDs = make([]uuid.UUID, len(from.BankAccounts)) + for i, bankAccount := range from.BankAccounts { + psu.BankAccountIDs[i] = bankAccount.ID } return psu diff --git a/internal/storage/psu_test.go b/internal/storage/psu_test.go index 6cea83c3f..a1dbe81da 100644 --- a/internal/storage/psu_test.go +++ b/internal/storage/psu_test.go @@ -55,7 +55,7 @@ var ( PostalCode: pointer.For("test"), Country: pointer.For("test"), }, - BankAccountIDs: []uuid.UUID{defaultBankAccount.ID}, + BankAccountIDs: []uuid.UUID{defaultBankAccount2.ID}, } ) @@ -73,8 +73,6 @@ func TestPSUCreate(t *testing.T) { upsertBankAccount(t, ctx, store, defaultBankAccount) upsertAccounts(t, ctx, store, defaultAccounts()) createPSU(t, ctx, store, defaultPSU) - createPSU(t, ctx, store, defaultPSU2) - createPSU(t, ctx, store, defaultPSU3) t.Run("upsert with same id", func(t *testing.T) { psu := models.PaymentServiceUser{ @@ -103,7 +101,7 @@ func TestPSUCreate(t *testing.T) { comparePSUs(t, defaultPSU, *actual) }) - t.Run("unknown psu id", func(t *testing.T) { + t.Run("unknown bank account id id", func(t *testing.T) { cp := models.PaymentServiceUser{ ID: uuid.New(), Name: "test", @@ -124,6 +122,7 @@ func TestPSUGet(t *testing.T) { store := newStore(t) upsertConnector(t, ctx, store, defaultConnector) upsertBankAccount(t, ctx, store, defaultBankAccount) + upsertBankAccount(t, ctx, store, defaultBankAccount2) upsertAccounts(t, ctx, store, defaultAccounts()) createPSU(t, ctx, store, defaultPSU) createPSU(t, ctx, store, defaultPSU2) @@ -155,6 +154,7 @@ func TestPSUList(t *testing.T) { store := newStore(t) upsertConnector(t, ctx, store, defaultConnector) upsertBankAccount(t, ctx, store, defaultBankAccount) + upsertBankAccount(t, ctx, store, defaultBankAccount2) upsertAccounts(t, ctx, store, defaultAccounts()) createPSU(t, ctx, store, defaultPSU) createPSU(t, ctx, store, defaultPSU2) @@ -316,8 +316,6 @@ func TestPSUAddBankAccount(t *testing.T) { upsertBankAccount(t, ctx, store, defaultBankAccount2) upsertAccounts(t, ctx, store, defaultAccounts()) createPSU(t, ctx, store, defaultPSU) - createPSU(t, ctx, store, defaultPSU2) - createPSU(t, ctx, store, defaultPSU3) t.Run("add bank account to psu", func(t *testing.T) { err := store.PaymentServiceUsersAddBankAccount(ctx, defaultPSU.ID, defaultBankAccount2.ID) diff --git a/test/e2e/api_payment_service_users_test.go b/test/e2e/api_payment_service_users_test.go index 8f208cae1..ca18fca37 100644 --- a/test/e2e/api_payment_service_users_test.go +++ b/test/e2e/api_payment_service_users_test.go @@ -45,8 +45,8 @@ var _ = Context("Payment API Payment Service Users", func() { v3CreateRequest = &components.V3CreatePaymentServiceUserRequest{ Name: "test", ContactDetails: &components.V3ContactDetailsRequest{ - Email: pointer.For("test"), - PhoneNumber: pointer.For("test"), + Email: pointer.For("dev@formance.com"), + PhoneNumber: pointer.For("+33612131415"), }, Address: &components.V3AddressRequest{ StreetNumber: pointer.For("1"), @@ -147,7 +147,7 @@ var _ = Context("Payment API Payment Service Users", func() { It("should fail if bank account does not exists", func() { _, err := app.GetValue().SDK().Payments.V3.AddBankAccountToPaymentServiceUser(ctx, psuID, uuid.New().String()) Expect(err).To(HaveOccurred()) - Expect(err.Error()).To(ContainSubstring("failed to add bank account to payment service user: bank_account_id: value not found")) + Expect(err.Error()).To(ContainSubstring("failed to add bank account to payment service user: bank account: not found")) }) It("should fail if payment service user does not exists", func() { @@ -233,8 +233,8 @@ var _ = Context("Payment API Payment Service Users", func() { Metadata: map[string]string{ "com.formance.spec/owner/addressLine1": "1 test", "com.formance.spec/owner/city": "test", - "com.formance.spec/owner/email": "test", - "com.formance.spec/owner/phoneNumber": "test", + "com.formance.spec/owner/email": "dev@formance.com", + "com.formance.spec/owner/phoneNumber": "+33612131415", "com.formance.spec/owner/postalCode": "test", "com.formance.spec/owner/region": "test", "com.formance.spec/owner/streetName": "test", From 4ba031d413d841a9ea30c30e344038e5c28fe78c Mon Sep 17 00:00:00 2001 From: Paul Nicolas Date: Mon, 28 Apr 2025 16:33:27 +0200 Subject: [PATCH 12/13] remove duplicate test --- internal/storage/psu_test.go | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/internal/storage/psu_test.go b/internal/storage/psu_test.go index a1dbe81da..9dedc0284 100644 --- a/internal/storage/psu_test.go +++ b/internal/storage/psu_test.go @@ -234,22 +234,6 @@ func TestPSUList(t *testing.T) { require.Empty(t, cursor.Next) }) - t.Run("list psu by metadata", func(t *testing.T) { - q := NewListPSUQuery( - bunpaginate.NewPaginatedQueryOptions(PSUQuery{}). - WithPageSize(15). - WithQueryBuilder(query.Match("metadata[foo]", "bar")), - ) - - cursor, err := store.PaymentServiceUsersList(ctx, q) - require.NoError(t, err) - require.Len(t, cursor.Data, 1) - require.False(t, cursor.HasMore) - require.Empty(t, cursor.Previous) - require.Empty(t, cursor.Next) - comparePSUs(t, defaultPSU, cursor.Data[0]) - }) - t.Run("list psu test cursor", func(t *testing.T) { q := NewListPSUQuery( bunpaginate.NewPaginatedQueryOptions(PSUQuery{}). From 8dfd4a7e8b9d0e88e1cbcfa9312a96f7c5ff0ef7 Mon Sep 17 00:00:00 2001 From: Paul Nicolas Date: Mon, 28 Apr 2025 19:41:27 +0200 Subject: [PATCH 13/13] remove default when adding the psu id column --- internal/storage/migrations/14-create-psu-tables.sql | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/storage/migrations/14-create-psu-tables.sql b/internal/storage/migrations/14-create-psu-tables.sql index fce108114..1ebad9939 100644 --- a/internal/storage/migrations/14-create-psu-tables.sql +++ b/internal/storage/migrations/14-create-psu-tables.sql @@ -28,7 +28,7 @@ create table if not exists payment_service_users ( create index psu_created_at_sort_id on payment_service_users (created_at, sort_id); alter table bank_accounts - add column if not exists psu_id uuid default null; + add column if not exists psu_id uuid; alter table bank_accounts add constraint fk_psu_id