Skip to content

Commit

Permalink
feat(payments): handle currencies properly (#871)
Browse files Browse the repository at this point in the history
  • Loading branch information
paul-nicolas authored Nov 20, 2023
1 parent 84fae1f commit 4fc380d
Show file tree
Hide file tree
Showing 49 changed files with 883 additions and 189 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"time"

"github.com/ThreeDotsLabs/watermill/message"
"github.com/formancehq/payments/cmd/connectors/internal/integration"
"github.com/formancehq/payments/cmd/connectors/internal/messages"
"github.com/formancehq/payments/internal/models"
"github.com/formancehq/payments/pkg/events"
Expand Down Expand Up @@ -50,7 +51,7 @@ type createTransferInitiationRequest struct {
Validated bool `json:"validated"`
}

func (r *createTransferInitiationRequest) Validate(repo createTransferInitiationRepository) error {
func (r *createTransferInitiationRequest) Validate() error {
if r.Reference == "" {
return errors.New("uniqueRequestId is required")
}
Expand Down Expand Up @@ -109,7 +110,7 @@ func createTransferInitiationHandler(
return
}

if err := payload.Validate(repo); err != nil {
if err := payload.Validate(); err != nil {
api.BadRequest(w, ErrValidation, err)
return
}
Expand All @@ -119,6 +120,7 @@ func createTransferInitiationHandler(
status = models.TransferInitiationStatusValidated
}

fmt.Println("TOTO 1")
var connectorID models.ConnectorID
if payload.ConnectorID == "" {
provider, err := models.ConnectorProviderFromString(payload.Provider)
Expand All @@ -145,15 +147,22 @@ func createTransferInitiationHandler(

connectorID = connectors[0].ID
} else {
connectorID = models.MustConnectorIDFromString(payload.ConnectorID)
var err error
connectorID, err = models.ConnectorIDFromString(payload.ConnectorID)
if err != nil {
api.BadRequest(w, ErrValidation, err)
return
}
}

fmt.Println("TOTO 2")
isInstalled, _ := repo.IsInstalledByConnectorID(r.Context(), connectorID)
if !isInstalled {
api.BadRequest(w, ErrValidation, fmt.Errorf("connector %s is not installed", payload.ConnectorID))
return
}

fmt.Println("TOTO 3")
if payload.SourceAccountID != "" {
_, err := repo.GetAccount(r.Context(), payload.SourceAccountID)
if err != nil {
Expand All @@ -162,12 +171,14 @@ func createTransferInitiationHandler(
}
}

fmt.Println("TOTO 4")
_, err := repo.GetAccount(r.Context(), payload.DestinationAccountID)
if err != nil {
handleStorageErrors(w, r, fmt.Errorf("failed to get destination account: %w", err))
return
}

fmt.Println("TOTO 5")
if payload.ScheduledAt.IsZero() {
payload.ScheduledAt = time.Now().UTC()
}
Expand All @@ -184,6 +195,7 @@ func createTransferInitiationHandler(
Description: payload.Description,
DestinationAccountID: models.MustAccountIDFromString(payload.DestinationAccountID),
ConnectorID: connectorID,
Provider: connectorID.Provider,
Type: models.MustTransferInitiationTypeFromString(payload.Type),
Amount: payload.Amount,
Asset: models.Asset(payload.Asset),
Expand All @@ -194,11 +206,13 @@ func createTransferInitiationHandler(
tf.SourceAccountID = models.MustAccountIDFromString(payload.SourceAccountID)
}

fmt.Println("TOTO 6")
if err := repo.CreateTransferInitiation(r.Context(), tf); err != nil {
handleStorageErrors(w, r, err)
return
}

fmt.Println("TOTO 7")
if err := publisher.Publish(
events.TopicPayments,
publish.NewMessage(
Expand All @@ -210,6 +224,7 @@ func createTransferInitiationHandler(
return
}

fmt.Println("TOTO 8")
if status == models.TransferInitiationStatusValidated {
connector, err := repo.GetConnector(r.Context(), connectorID)
if err != nil {
Expand All @@ -225,11 +240,19 @@ func createTransferInitiationHandler(

err = f(r.Context(), tf)
if err != nil {
api.InternalServerError(w, r, err)
switch {
case errors.Is(err, integration.ErrValidation):
api.BadRequest(w, ErrValidation, err)
case errors.Is(err, integration.ErrConnectorNotFound):
api.BadRequest(w, ErrValidation, err)
default:
api.InternalServerError(w, r, err)
}
return
}
}

fmt.Println("TOTO 9")
data := &transferInitiationResponse{
ID: tf.ID.String(),
CreatedAt: tf.CreatedAt,
Expand Down Expand Up @@ -341,7 +364,14 @@ func updateTransferInitiationStatusHandler(

err = f(r.Context(), previousTransferInitiation)
if err != nil {
api.InternalServerError(w, r, err)
switch {
case errors.Is(err, integration.ErrValidation):
api.BadRequest(w, ErrValidation, err)
case errors.Is(err, integration.ErrConnectorNotFound):
api.BadRequest(w, ErrValidation, err)
default:
api.InternalServerError(w, r, err)
}
return
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,10 @@ func (c *Connector) InitiatePayment(ctx task.ConnectorContext, transfer *models.
return nil
}

func (c *Connector) SupportedCurrenciesAndDecimals() map[string]int {
return supportedCurrenciesWithDecimal
}

func (c *Connector) Install(ctx task.ConnectorContext) error {
taskDescriptor, err := models.EncodeTaskDescriptor(TaskDescriptor{
Name: "Main task to periodically fetch payments",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package bankingcircle

import "github.com/formancehq/payments/cmd/connectors/internal/connectors/currency"

var (
// All supported BankingCircle currencies and decimal are on par with
// ISO4217Currencies.
supportedCurrenciesWithDecimal = map[string]int{
"AED": currency.ISO4217Currencies["AED"], // UAE Dirham
"AUD": currency.ISO4217Currencies["AUD"], // Australian Dollar
"CAD": currency.ISO4217Currencies["CAD"], // Canadian Dollar
"CHF": currency.ISO4217Currencies["CHF"], // Swiss Franc
"CNY": currency.ISO4217Currencies["CNY"], // China Yuan Renminbi
"CZK": currency.ISO4217Currencies["CZK"], // Czech Koruna
"DKK": currency.ISO4217Currencies["DKK"], // Danish Krone
"EUR": currency.ISO4217Currencies["EUR"], // Euro
"GBP": currency.ISO4217Currencies["GBP"], // Pound Sterling
"HKD": currency.ISO4217Currencies["HKD"], // Hong Kong Dollar
"ILS": currency.ISO4217Currencies["ILS"], // Israeli Shekel
"JPY": currency.ISO4217Currencies["JPY"], // Japanese Yen
"MXN": currency.ISO4217Currencies["MXN"], // Mexican Peso
"NOK": currency.ISO4217Currencies["NOK"], // Norwegian Krone
"NZD": currency.ISO4217Currencies["NZD"], // New Zealand Dollar
"PLN": currency.ISO4217Currencies["PLN"], // Polish Zloty
"RON": currency.ISO4217Currencies["RON"], // Romanian Leu
"SAR": currency.ISO4217Currencies["SAR"], // Saudi Riyal
"SEK": currency.ISO4217Currencies["SEK"], // Swedish Krona
"SGD": currency.ISO4217Currencies["SGD"], // Singapore Dollar
"TRY": currency.ISO4217Currencies["TRY"], // Turkish Lira
"USD": currency.ISO4217Currencies["USD"], // US Dollar
"ZAR": currency.ISO4217Currencies["ZAR"], // South African Rand

// Unsupported currencies
// Since we're not sure about decimals for these currencies, we prefer
// to not support them for now.
// "HUF": 2, // Hungarian Forint
}
)
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,12 @@ import (
"encoding/json"
"errors"
"fmt"
"math"
"math/big"
"time"

"github.com/formancehq/payments/cmd/connectors/internal/connectors/bankingcircle/client"
"github.com/formancehq/payments/cmd/connectors/internal/connectors/currency"
"github.com/formancehq/payments/cmd/connectors/internal/ingestion"
"github.com/formancehq/payments/cmd/connectors/internal/metrics"
"github.com/formancehq/payments/cmd/connectors/internal/task"
Expand Down Expand Up @@ -107,32 +109,33 @@ func ingestAccountsBatch(
CreatedAt: openingDate,
Reference: account.AccountID,
ConnectorID: connectorID,
DefaultAsset: models.Asset(account.Currency + "/2"),
DefaultAsset: currency.FormatAsset(supportedCurrenciesWithDecimal, account.Currency),
AccountName: account.AccountDescription,
Type: models.AccountTypeInternal,
RawData: raw,
})

for _, balance := range account.Balances {
// No need to check if the currency is supported for accounts and
// balances.
precision, _ := supportedCurrenciesWithDecimal[balance.Currency]

var amount big.Float
_, ok := amount.SetString(balance.IntraDayAmount.String())
if !ok {
return fmt.Errorf("failed to parse amount %s", balance.IntraDayAmount)
}

var amountInt big.Int
amount.Mul(&amount, big.NewFloat(100)).Int(&amountInt)
amount.Mul(&amount, big.NewFloat(math.Pow(10, float64(precision)))).Int(&amountInt)

now := time.Now()
balanceBatch = append(balanceBatch, &models.Balance{
AccountID: models.AccountID{
Reference: account.AccountID,
ConnectorID: connectorID,
},
// Note(polo): same thing as payments
// TODO(polo): do a complete pass on all connectors to
// normalize the currencies
Asset: models.Asset(balance.Currency + "/2"),
Asset: currency.FormatAsset(supportedCurrenciesWithDecimal, balance.Currency),
Balance: &amountInt,
CreatedAt: now,
LastUpdatedAt: now,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,12 @@ package bankingcircle
import (
"context"
"encoding/json"
"math"
"math/big"
"time"

"github.com/formancehq/payments/cmd/connectors/internal/connectors/bankingcircle/client"
"github.com/formancehq/payments/cmd/connectors/internal/connectors/currency"
"github.com/formancehq/payments/cmd/connectors/internal/ingestion"
"github.com/formancehq/payments/cmd/connectors/internal/metrics"
"github.com/formancehq/payments/cmd/connectors/internal/task"
Expand Down Expand Up @@ -46,7 +48,7 @@ func taskFetchPayments(
break
}

if err := ingestBatch(ctx, connectorID, ingester, pagedPayments); err != nil {
if err := ingestBatch(ctx, logger, metricsRegistry, connectorID, ingester, pagedPayments); err != nil {
metricsRegistry.ConnectorObjectsErrors().Add(ctx, 1, paymentsAttrs)
return err
}
Expand All @@ -59,6 +61,8 @@ func taskFetchPayments(

func ingestBatch(
ctx context.Context,
logger logging.Logger,
metricsRegistry metrics.MetricsRegistry,
connectorID models.ConnectorID,
ingester ingestion.Ingester,
payments []*client.Payment,
Expand All @@ -73,11 +77,17 @@ func ingestBatch(

paymentType := matchPaymentType(paymentEl.Classification)

precision, ok := supportedCurrenciesWithDecimal[paymentEl.Transfer.Amount.Currency]
if !ok {
logger.Errorf("currency %s is not supported", paymentEl.Transfer.Amount.Currency)
metricsRegistry.ConnectorCurrencyNotSupported().Add(ctx, 1, metric.WithAttributes(connectorAttrs...))
continue
}

var amount big.Float
amount.SetFloat64(paymentEl.Transfer.Amount.Amount)

var amountInt big.Int
amount.Mul(&amount, big.NewFloat(100)).Int(&amountInt)
amount.Mul(&amount, big.NewFloat(math.Pow(10, float64(precision)))).Int(&amountInt)

batchElement := ingestion.PaymentBatchElement{
Payment: &models.Payment{
Expand All @@ -94,7 +104,7 @@ func ingestBatch(
Status: matchPaymentStatus(paymentEl.Status),
Scheme: models.PaymentSchemeOther,
Amount: &amountInt,
Asset: models.Asset(paymentEl.Transfer.Amount.Currency + "/2"),
Asset: currency.FormatAsset(supportedCurrenciesWithDecimal, paymentEl.Transfer.Amount.Currency),
RawData: raw,
},
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package bankingcircle
import (
"context"
"errors"
"math"
"math/big"
"time"

Expand Down Expand Up @@ -82,13 +83,14 @@ func taskInitiatePayment(logger logging.Logger, bankingCircleClient *client.Clie
}

var curr string
curr, _, err = currency.GetCurrencyAndPrecisionFromAsset(transfer.Asset)
var precision int
curr, precision, err = currency.GetCurrencyAndPrecisionFromAsset(supportedCurrenciesWithDecimal, transfer.Asset)
if err != nil {
return err
}

amount := big.NewFloat(0).SetInt(transfer.Amount)
amount = amount.Quo(amount, big.NewFloat(100))
amount = amount.Quo(amount, big.NewFloat(math.Pow(10, float64(precision))))

var sourceAccount *client.Account
sourceAccount, err = bankingCircleClient.GetAccount(ctx, transfer.SourceAccountID.Reference)
Expand Down
Loading

1 comment on commit 4fc380d

@vercel
Copy link

@vercel vercel bot commented on 4fc380d Nov 20, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.