Skip to content

Commit

Permalink
feat(payments): add mangopay bank accounts creation (#1159)
Browse files Browse the repository at this point in the history
  • Loading branch information
paul-nicolas authored Jan 30, 2024
1 parent 6cd67f8 commit dbfa498
Show file tree
Hide file tree
Showing 16 changed files with 396 additions and 15 deletions.
3 changes: 2 additions & 1 deletion components/fctl/cmd/payments/bankaccounts/list.go
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,7 @@ func (c *ListController) Render(cmd *cobra.Command, args []string) error {
tableData := fctl.Map(c.store.Cursor.Data, func(bc shared.BankAccount) []string {
return []string{
bc.ID,
bc.Name,
bc.CreatedAt.Format(time.RFC3339),
bc.Country,
string(bc.ConnectorID),
Expand All @@ -125,7 +126,7 @@ func (c *ListController) Render(cmd *cobra.Command, args []string) error {
}(),
}
})
tableData = fctl.Prepend(tableData, []string{"ID", "CreatedAt", "Country", "ConnectorID", "Provider"})
tableData = fctl.Prepend(tableData, []string{"ID", "Name", "CreatedAt", "Country", "ConnectorID", "Provider"})
return pterm.DefaultTable.
WithHasHeader().
WithWriter(cmd.OutOrStdout()).
Expand Down
1 change: 1 addition & 0 deletions components/fctl/cmd/payments/bankaccounts/show.go
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@ func (c *ShowController) Render(cmd *cobra.Command, args []string) error {
fctl.Section.WithWriter(cmd.OutOrStdout()).Println("Information")
tableData := pterm.TableData{}
tableData = append(tableData, []string{pterm.LightCyan("ID"), c.store.BankAccount.ID})
tableData = append(tableData, []string{pterm.LightCyan("Name"), c.store.BankAccount.Name})
tableData = append(tableData, []string{pterm.LightCyan("CreatedAt"), c.store.BankAccount.CreatedAt.Format(time.RFC3339)})
tableData = append(tableData, []string{pterm.LightCyan("Country"), c.store.BankAccount.Country})
tableData = append(tableData, []string{pterm.LightCyan("ConnectorID"), string(c.store.BankAccount.ConnectorID)})
Expand Down
3 changes: 3 additions & 0 deletions components/payments/cmd/api/internal/api/bank_accounts.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import (

type bankAccountResponse struct {
ID string `json:"id"`
Name string `json:"name"`
CreatedAt time.Time `json:"createdAt"`
Country string `json:"country"`
ConnectorID string `json:"connectorID"`
Expand Down Expand Up @@ -45,6 +46,7 @@ func listBankAccountsHandler(b backend.Backend) http.HandlerFunc {
for i := range ret {
data[i] = &bankAccountResponse{
ID: ret[i].ID.String(),
Name: ret[i].Name,
CreatedAt: ret[i].CreatedAt,
Country: ret[i].Country,
ConnectorID: ret[i].ConnectorID.String(),
Expand Down Expand Up @@ -93,6 +95,7 @@ func readBankAccountHandler(b backend.Backend) http.HandlerFunc {

data := &bankAccountResponse{
ID: account.ID.String(),
Name: account.Name,
CreatedAt: account.CreatedAt,
Country: account.Country,
ConnectorID: account.ConnectorID.String(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,7 @@ func TestListBankAccounts(t *testing.T) {
expectedBankAccountsResponse := []*bankAccountResponse{
{
ID: listBankAccountsResponse[0].ID.String(),
Name: listBankAccountsResponse[0].Name,
CreatedAt: listBankAccountsResponse[0].CreatedAt,
Country: listBankAccountsResponse[0].Country,
ConnectorID: listBankAccountsResponse[0].ConnectorID.String(),
Expand All @@ -192,6 +193,7 @@ func TestListBankAccounts(t *testing.T) {
},
{
ID: listBankAccountsResponse[1].ID.String(),
Name: listBankAccountsResponse[1].Name,
CreatedAt: listBankAccountsResponse[1].CreatedAt,
Country: listBankAccountsResponse[1].Country,
ConnectorID: listBankAccountsResponse[1].ConnectorID.String(),
Expand Down Expand Up @@ -333,6 +335,7 @@ func TestGetBankAccount(t *testing.T) {

expectedBankAccountResponse := &bankAccountResponse{
ID: getBankAccountResponse.ID.String(),
Name: getBankAccountResponse.Name,
CreatedAt: getBankAccountResponse.CreatedAt,
Country: getBankAccountResponse.Country,
ConnectorID: getBankAccountResponse.ConnectorID.String(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import (

type bankAccountResponse struct {
ID string `json:"id"`
Name string `json:"name"`
CreatedAt time.Time `json:"createdAt"`
Country string `json:"country"`
ConnectorID string `json:"connectorID"`
Expand Down Expand Up @@ -63,6 +64,7 @@ func createBankAccountHandler(

data := &bankAccountResponse{
ID: bankAccount.ID.String(),
Name: bankAccount.Name,
CreatedAt: bankAccount.CreatedAt,
Country: bankAccount.Country,
ConnectorID: bankAccountRequest.ConnectorID,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,7 @@ func TestCreateBankAccounts(t *testing.T) {

expectedCreateBankAccountResponse := &bankAccountResponse{
ID: createBankAccountResponse.ID.String(),
Name: createBankAccountResponse.Name,
CreatedAt: createBankAccountResponse.CreatedAt,
Country: createBankAccountResponse.Country,
ConnectorID: createBankAccountResponse.ConnectorID.String(),
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package client

import (
"bytes"
"context"
"encoding/json"
"fmt"
Expand All @@ -11,13 +12,150 @@ import (
"github.com/formancehq/payments/cmd/connectors/internal/connectors"
)

type bankAccount struct {
type OwnerAddress struct {
AddressLine1 string `json:"AddressLine1,omitempty"`
AddressLine2 string `json:"AddressLine2,omitempty"`
City string `json:"City,omitempty"`
// Region is needed if country is either US, CA or MX
Region string `json:"Region,omitempty"`
PostalCode string `json:"PostalCode,omitempty"`
// ISO 3166-1 alpha-2 format.
Country string `json:"Country,omitempty"`
}

type CreateIBANBankAccountRequest struct {
OwnerName string `json:"OwnerName"`
OwnerAddress *OwnerAddress `json:"OwnerAddress,omitempty"`
IBAN string `json:"IBAN,omitempty"`
BIC string `json:"BIC,omitempty"`
// Metadata
Tag string `json:"Tag,omitempty"`
}

func (c *Client) CreateIBANBankAccount(ctx context.Context, userID string, req *CreateIBANBankAccountRequest) (*BankAccount, error) {
f := connectors.ClientMetrics(ctx, "mangopay", "create_iban_bank_account")
now := time.Now()
defer f(ctx, now)

endpoint := fmt.Sprintf("%s/v2.01/%s/users/%s/bankaccounts/iban", c.endpoint, c.clientID, userID)
return c.createBankAccount(ctx, endpoint, req)
}

type CreateUSBankAccountRequest struct {
OwnerName string `json:"OwnerName"`
OwnerAddress *OwnerAddress `json:"OwnerAddress,omitempty"`
AccountNumber string `json:"AccountNumber"`
ABA string `json:"ABA"`
DepositAccountType string `json:"DepositAccountType,omitempty"`
Tag string `json:"Tag,omitempty"`
}

func (c *Client) CreateUSBankAccount(ctx context.Context, userID string, req *CreateUSBankAccountRequest) (*BankAccount, error) {
f := connectors.ClientMetrics(ctx, "mangopay", "create_us_bank_account")
now := time.Now()
defer f(ctx, now)

endpoint := fmt.Sprintf("%s/v2.01/%s/users/%s/bankaccounts/us", c.endpoint, c.clientID, userID)
return c.createBankAccount(ctx, endpoint, req)
}

type CreateCABankAccountRequest struct {
OwnerName string `json:"OwnerName"`
OwnerAddress *OwnerAddress `json:"OwnerAddress,omitempty"`
AccountNumber string `json:"AccountNumber"`
InstitutionNumber string `json:"InstitutionNumber"`
BranchCode string `json:"BranchCode"`
BankName string `json:"BankName"`
Tag string `json:"Tag,omitempty"`
}

func (c *Client) CreateCABankAccount(ctx context.Context, userID string, req *CreateCABankAccountRequest) (*BankAccount, error) {
f := connectors.ClientMetrics(ctx, "mangopay", "create_ca_bank_account")
now := time.Now()
defer f(ctx, now)

endpoint := fmt.Sprintf("%s/v2.01/%s/users/%s/bankaccounts/ca", c.endpoint, c.clientID, userID)
return c.createBankAccount(ctx, endpoint, req)
}

type CreateGBBankAccountRequest struct {
OwnerName string `json:"OwnerName"`
OwnerAddress *OwnerAddress `json:"OwnerAddress,omitempty"`
AccountNumber string `json:"AccountNumber"`
SortCode string `json:"SortCode"`
Tag string `json:"Tag,omitempty"`
}

func (c *Client) CreateGBBankAccount(ctx context.Context, userID string, req *CreateGBBankAccountRequest) (*BankAccount, error) {
f := connectors.ClientMetrics(ctx, "mangopay", "create_gb_bank_account")
now := time.Now()
defer f(ctx, now)

endpoint := fmt.Sprintf("%s/v2.01/%s/users/%s/bankaccounts/gb", c.endpoint, c.clientID, userID)
return c.createBankAccount(ctx, endpoint, req)
}

type CreateOtherBankAccountRequest struct {
OwnerName string `json:"OwnerName"`
OwnerAddress *OwnerAddress `json:"OwnerAddress,omitempty"`
AccountNumber string `json:"AccountNumber"`
BIC string `json:"BIC,omitempty"`
Country string `json:"Country,omitempty"`
Tag string `json:"Tag,omitempty"`
}

func (c *Client) CreateOtherBankAccount(ctx context.Context, userID string, req *CreateOtherBankAccountRequest) (*BankAccount, error) {
f := connectors.ClientMetrics(ctx, "mangopay", "create_other_bank_account")
now := time.Now()
defer f(ctx, now)

endpoint := fmt.Sprintf("%s/v2.01/%s/users/%s/bankaccounts/other", c.endpoint, c.clientID, userID)
return c.createBankAccount(ctx, endpoint, req)
}

func (c *Client) createBankAccount(ctx context.Context, endpoint string, req any) (*BankAccount, error) {
body, err := json.Marshal(req)
if err != nil {
return nil, fmt.Errorf("failed to marshal bank account request: %w", err)
}

httpReq, err := http.NewRequestWithContext(ctx, http.MethodPost, endpoint, bytes.NewBuffer(body))
if err != nil {
return nil, fmt.Errorf("failed to create bank account request: %w", err)
}
httpReq.Header.Set("Content-Type", "application/json")

resp, err := c.httpClient.Do(httpReq)
if err != nil {
return nil, fmt.Errorf("failed to create bank account: %w", err)
}

defer func() {
err = resp.Body.Close()
if err != nil {
c.logger.Error(err)
}
}()

if resp.StatusCode != http.StatusOK {
return nil, unmarshalError(resp.StatusCode, resp.Body).Error()
}

var bankAccount BankAccount
if err := json.NewDecoder(resp.Body).Decode(&bankAccount); err != nil {
return nil, fmt.Errorf("failed to unmarshal bank account response body: %w", err)
}

return &bankAccount, nil
}

type BankAccount struct {
ID string `json:"Id"`
OwnerName string `json:"OwnerName"`
CreationDate int64 `json:"CreationDate"`
}

func (c *Client) GetBankAccounts(ctx context.Context, userID string, page, pageSize int) ([]*bankAccount, error) {
func (c *Client) GetBankAccounts(ctx context.Context, userID string, page, pageSize int) ([]*BankAccount, error) {
f := connectors.ClientMetrics(ctx, "mangopay", "list_bank_accounts")
now := time.Now()
defer f(ctx, now)
Expand Down Expand Up @@ -50,7 +188,7 @@ func (c *Client) GetBankAccounts(ctx context.Context, userID string, page, pageS
return nil, unmarshalError(resp.StatusCode, resp.Body).Error()
}

var bankAccounts []*bankAccount
var bankAccounts []*BankAccount
if err := json.NewDecoder(resp.Body).Decode(&bankAccounts); err != nil {
return nil, fmt.Errorf("failed to unmarshal wallets response body: %w", err)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package client

const (
mangopayMetadataSpecNamespace = "com.mangopay.spec/"
)

func ExtractNamespacedMetadata(metadata map[string]string, key string) string {
value, ok := metadata[mangopayMetadataSpecNamespace+key]
if !ok {
return ""
}
return value
}
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,21 @@ func (c *Connector) ReversePayment(ctx task.ConnectorContext, transferReversal *
}

func (c *Connector) CreateExternalBankAccount(ctx task.ConnectorContext, bankAccount *models.BankAccount) error {
return connectors.ErrNotImplemented
descriptor, err := models.EncodeTaskDescriptor(TaskDescriptor{
Name: "Create external bank account",
Key: taskNameCreateExternalBankAccount,
BankAccountID: bankAccount.ID,
})
if err != nil {
return err
}
if err := ctx.Scheduler().Schedule(ctx.Context(), descriptor, models.TaskSchedulerOptions{
ScheduleOption: models.OPTIONS_RUN_NOW_SYNC,
}); err != nil {
return err
}

return nil
}

var _ connectors.Connector = &Connector{}
Loading

0 comments on commit dbfa498

Please sign in to comment.