From 4fc380d24d0a54aaf689147fc727cfe6402f1e28 Mon Sep 17 00:00:00 2001 From: Paul Nicolas Date: Mon, 20 Nov 2023 17:24:53 +0100 Subject: [PATCH] feat(payments): handle currencies properly (#871) --- .../internal/api/transfer_initiation.go | 40 ++- .../connectors/bankingcircle/connector.go | 4 + .../connectors/bankingcircle/currencies.go | 38 +++ .../bankingcircle/task_fetch_accounts.go | 15 +- .../bankingcircle/task_fetch_payments.go | 18 +- .../connectors/bankingcircle/task_payments.go | 6 +- .../internal/connectors/currency/currency.go | 302 +++++++++++------- .../connectors/currencycloud/connector.go | 5 +- .../connectors/currencycloud/currencies.go | 51 +++ .../currencycloud/task_fetch_balances.go | 9 +- .../currencycloud/task_fetch_transactions.go | 25 +- .../connectors/currencycloud/task_payments.go | 6 +- .../internal/connectors/dummypay/connector.go | 4 + .../connectors/dummypay/currencies.go | 7 + .../internal/connectors/mangopay/connector.go | 4 + .../connectors/mangopay/currencies.go | 24 ++ .../mangopay/task_fetch_transactions.go | 2 +- .../connectors/mangopay/task_fetch_wallets.go | 4 +- .../connectors/mangopay/task_payments.go | 4 +- .../internal/connectors/modulr/connector.go | 4 + .../internal/connectors/modulr/currencies.go | 20 ++ .../connectors/modulr/task_fetch_accounts.go | 12 +- .../modulr/task_fetch_transactions.go | 16 +- .../connectors/modulr/task_payments.go | 6 +- .../connectors/moneycorp/connector.go | 4 + .../connectors/moneycorp/currencies.go | 63 ++++ .../moneycorp/task_fetch_balances.go | 4 +- .../moneycorp/task_fetch_recipients.go | 2 +- .../moneycorp/task_fetch_transactions.go | 4 +- .../connectors/moneycorp/task_payments.go | 2 +- .../internal/connectors/stripe/connector.go | 4 + .../internal/connectors/stripe/currencies.go | 162 ++++++++++ .../connectors/stripe/task_fetch_accounts.go | 2 +- .../connectors/stripe/task_fetch_balances.go | 2 +- .../stripe/task_fetch_external_accounts.go | 2 +- .../connectors/stripe/task_payments.go | 2 +- .../internal/connectors/stripe/translate.go | 28 +- .../internal/connectors/wise/connector.go | 4 + .../internal/connectors/wise/currencies.go | 33 ++ .../connectors/wise/task_fetch_profiles.go | 19 +- .../wise/task_fetch_recipient_accounts.go | 3 +- .../connectors/wise/task_fetch_transfers.go | 15 +- .../internal/connectors/wise/task_payments.go | 6 +- .../internal/integration/connector.go | 2 + .../internal/integration/connector_test.go | 4 + .../internal/integration/manager.go | 29 ++ .../connectors/internal/metrics/metrics.go | 33 +- .../cmd/connectors/internal/task/scheduler.go | 1 + .../payments/internal/models/payment.go | 16 + 49 files changed, 883 insertions(+), 189 deletions(-) create mode 100644 components/payments/cmd/connectors/internal/connectors/bankingcircle/currencies.go create mode 100644 components/payments/cmd/connectors/internal/connectors/currencycloud/currencies.go create mode 100644 components/payments/cmd/connectors/internal/connectors/dummypay/currencies.go create mode 100644 components/payments/cmd/connectors/internal/connectors/mangopay/currencies.go create mode 100644 components/payments/cmd/connectors/internal/connectors/modulr/currencies.go create mode 100644 components/payments/cmd/connectors/internal/connectors/moneycorp/currencies.go create mode 100644 components/payments/cmd/connectors/internal/connectors/stripe/currencies.go create mode 100644 components/payments/cmd/connectors/internal/connectors/wise/currencies.go diff --git a/components/payments/cmd/connectors/internal/api/transfer_initiation.go b/components/payments/cmd/connectors/internal/api/transfer_initiation.go index 61f73af400..d0d3cbfeac 100644 --- a/components/payments/cmd/connectors/internal/api/transfer_initiation.go +++ b/components/payments/cmd/connectors/internal/api/transfer_initiation.go @@ -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" @@ -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") } @@ -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 } @@ -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) @@ -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 { @@ -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() } @@ -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), @@ -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( @@ -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 { @@ -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, @@ -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 } } diff --git a/components/payments/cmd/connectors/internal/connectors/bankingcircle/connector.go b/components/payments/cmd/connectors/internal/connectors/bankingcircle/connector.go index d92ac49928..82cd495e01 100644 --- a/components/payments/cmd/connectors/internal/connectors/bankingcircle/connector.go +++ b/components/payments/cmd/connectors/internal/connectors/bankingcircle/connector.go @@ -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", diff --git a/components/payments/cmd/connectors/internal/connectors/bankingcircle/currencies.go b/components/payments/cmd/connectors/internal/connectors/bankingcircle/currencies.go new file mode 100644 index 0000000000..9ffb3e4de4 --- /dev/null +++ b/components/payments/cmd/connectors/internal/connectors/bankingcircle/currencies.go @@ -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 + } +) diff --git a/components/payments/cmd/connectors/internal/connectors/bankingcircle/task_fetch_accounts.go b/components/payments/cmd/connectors/internal/connectors/bankingcircle/task_fetch_accounts.go index c4d4d79a5d..b97b45a675 100644 --- a/components/payments/cmd/connectors/internal/connectors/bankingcircle/task_fetch_accounts.go +++ b/components/payments/cmd/connectors/internal/connectors/bankingcircle/task_fetch_accounts.go @@ -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" @@ -107,13 +109,17 @@ 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 { @@ -121,7 +127,7 @@ func ingestAccountsBatch( } 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{ @@ -129,10 +135,7 @@ func ingestAccountsBatch( 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, diff --git a/components/payments/cmd/connectors/internal/connectors/bankingcircle/task_fetch_payments.go b/components/payments/cmd/connectors/internal/connectors/bankingcircle/task_fetch_payments.go index f6ed880662..c9c3026188 100644 --- a/components/payments/cmd/connectors/internal/connectors/bankingcircle/task_fetch_payments.go +++ b/components/payments/cmd/connectors/internal/connectors/bankingcircle/task_fetch_payments.go @@ -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" @@ -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 } @@ -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, @@ -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{ @@ -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, }, } diff --git a/components/payments/cmd/connectors/internal/connectors/bankingcircle/task_payments.go b/components/payments/cmd/connectors/internal/connectors/bankingcircle/task_payments.go index c3236e36bc..50bfd4d577 100644 --- a/components/payments/cmd/connectors/internal/connectors/bankingcircle/task_payments.go +++ b/components/payments/cmd/connectors/internal/connectors/bankingcircle/task_payments.go @@ -3,6 +3,7 @@ package bankingcircle import ( "context" "errors" + "math" "math/big" "time" @@ -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) diff --git a/components/payments/cmd/connectors/internal/connectors/currency/currency.go b/components/payments/cmd/connectors/internal/connectors/currency/currency.go index a3ad08a5c1..3ec4e7ce67 100644 --- a/components/payments/cmd/connectors/internal/connectors/currency/currency.go +++ b/components/payments/cmd/connectors/internal/connectors/currency/currency.go @@ -8,153 +8,217 @@ import ( "github.com/formancehq/payments/internal/models" ) -type currency struct { - decimals int -} +var ( + UnsupportedCurrencies = map[string]struct{}{ + "HUF": {}, + "ISK": {}, + "TWD": {}, + } -func currencies() map[string]currency { - return map[string]currency{ - "AED": {2}, // UAE Dirham - "AMD": {2}, // Armenian Dram - "ANG": {2}, // Netherlands Antillian Guilder - "ARS": {2}, // Argentine Peso - "ATS": {2}, // Euro - "AUD": {2}, // Australian Dollar - "AWG": {2}, // Aruban Guilder - "BAM": {2}, // Bosnia and Herzegovina, Convertible Marks - "BDT": {2}, // Bangladesh, Taka - "BEF": {2}, // Euro - "BHD": {3}, // Bahraini Dinar - "BMD": {2}, // Bermudian Dollar - "BND": {2}, // Brunei Dollar - "BOB": {2}, // Bolivia, Boliviano - "BRL": {2}, // Brazilian Real - "BSD": {2}, // Bahamian Dollar - "BWP": {2}, // Botswana, Pula - "BZD": {2}, // Belize Dollar - "CAD": {2}, // Canadian Dollar - "CHF": {2}, // Swiss Franc - "CLP": {0}, // Chilean Peso - "CNY": {2}, // China Yuan Renminbi - "COP": {2}, // Colombian Peso - "CRC": {2}, // Costa Rican Colon - "CUC": {2}, // Cuban Convertible Peso - "CUP": {2}, // Cuban Peso - "CYP": {2}, // Cyprus Pound - "CZK": {2}, // Czech Koruna - "DEM": {2}, // Euro - "DKK": {2}, // Danish Krone - "DOP": {2}, // Dominican Peso - "EEK": {2}, // Euro - "EGP": {2}, // Egyptian Pound - "ESP": {2}, // Euro - "EUR": {2}, // Euro - "FIM": {2}, // Euro - "FRF": {2}, // Euro - "GBP": {2}, // Pound Sterling - "GHC": {2}, // Ghana, Cedi - "GIP": {2}, // Gibraltar Pound - "GRD": {2}, // Euro - "GTQ": {2}, // Guatemala, Quetzal - "HKD": {2}, // Hong Kong Dollar - "HNL": {2}, // Honduras, Lempira - "HRK": {2}, // Croatian Kuna - "HUF": {0}, // Hungary, Forint - "IDR": {2}, // Indonesia, Rupiah - "IEP": {2}, // Euro - "ILS": {2}, // New Israeli Shekel - "INR": {2}, // Indian Rupee - "IRR": {2}, // Iranian Rial - "ISK": {0}, // Iceland Krona - "ITL": {2}, // Euro - "JMD": {2}, // Jamaican Dollar - "JOD": {3}, // Jordanian Dinar - "JPY": {0}, // Japan, Yen - "KES": {2}, // Kenyan Shilling - "KRW": {0}, // South Korea, Won - "KWD": {3}, // Kuwaiti Dinar - "KYD": {2}, // Cayman Islands Dollar - "LBP": {0}, // Lebanese Pound - "LTL": {2}, // Lithuanian Litas - "LUF": {2}, // Euro - "LVL": {2}, // Latvian Lats - "MKD": {2}, // Macedonia, Denar - "MTL": {2}, // Maltese Lira - "MUR": {0}, // Mauritius Rupee - "MXN": {2}, // Mexican Peso - "MYR": {2}, // Malaysian Ringgit - "MZM": {2}, // Mozambique Metical - "NLG": {2}, // Euro - "NOK": {2}, // Norwegian Krone - "NPR": {2}, // Nepalese Rupee - "NZD": {2}, // New Zealand Dollar - "OMR": {3}, // Rial Omani - "PEN": {2}, // Peru, Nuevo Sol - "PHP": {2}, // Philippine Peso - "PKR": {2}, // Pakistan Rupee - "PLN": {2}, // Poland, Zloty - "PTE": {2}, // Euro - "ROL": {2}, // Romania, Old Leu - "RON": {2}, // Romania, New Leu - "RUB": {2}, // Russian Ruble - "SAR": {2}, // Saudi Riyal - "SEK": {2}, // Swedish Krona - "SGD": {2}, // Singapore Dollar - "SIT": {2}, // Slovenia, Tolar - "SKK": {2}, // Slovak Koruna - "SVC": {2}, // El Salvador Colon - "SZL": {2}, // Swaziland, Lilangeni - "THB": {2}, // Thailand, Baht - "TOP": {2}, // Tonga, Paanga - "TRY": {2}, // New Turkish Lira - "TZS": {2}, // Tanzanian Shilling - "UAH": {2}, // Ukraine, Hryvnia - "USD": {2}, // US Dollar - "UYU": {2}, // Peso Uruguayo - "VEB": {2}, // Venezuela, Bolivar - "VEF": {2}, // Venezuela Bolivares Fuertes - "VND": {0}, // Viet Nam, Dong - "VUV": {0}, // Vanuatu, Vatu - "XCD": {2}, // East Caribbean Dollar - "ZAR": {2}, // South Africa, Rand - "ZWD": {2}, // Zimbabwe Dollar + ISO4217Currencies = map[string]int{ + "AFN": 2, // Afghan afghani + "EUR": 2, // Euro + "ALL": 2, // Albanian lek + "DZD": 2, // Algerian dinar + "USD": 2, // United States dollar + "AOA": 2, // Angolan kwanza + "XCD": 2, // East Caribbean dollar + "ARS": 2, // Argentine peso + "AMD": 2, // Armenian dram + "AWG": 2, // Aruban florin + "AUD": 2, // Australian dollar + "AZN": 2, // Azerbaijani manat + "BSD": 2, // Bahamian dollar + "BHD": 3, // Bahraini dinar + "BDT": 2, // Bangladeshi taka + "BBD": 2, // Barbados dollar + "BYN": 2, // Belarusian ruble + "BZD": 2, // Belize dollar + "XOF": 0, // West African CFA franc + "BMD": 2, // Bermudian dollar + "INR": 2, // Indian rupee + "BTN": 2, // Bhutanese ngultrum + "BOB": 2, // Bolivian boliviano + "BOV": 2, // Bolivian Mvdol (funds code) + "BAM": 2, // Bosnia and Herzegovina convertible mark + "BWP": 2, // Botswana pula + "NOK": 2, // Norwegian krone + "BRL": 2, // Brazilian real + "BND": 2, // Brunei dollar + "BGN": 2, // Bulgarian lev + "BIF": 0, // Burundian franc + "CVE": 2, // Cape Verdean escudo + "KHR": 2, // Cambodian riel + "XAF": 0, // Central African CFA franc + "CAD": 2, // Canadian dollar + "KYD": 2, // Cayman Islands dollar + "CLP": 0, // Chilean peso + "CLF": 4, // Unidad de Fomento (funds code) + "CNY": 2, // Chinese yuan + "COP": 2, // Colombian peso + "COU": 2, // Unidad de Valor Real (UVR) (funds code)[7] + "KMF": 0, // Comoro franc + "CDF": 2, // Congolese franc + "NZD": 2, // New Zealand dollar + "CRC": 2, // Costa Rican colon + "HRK": 2, // Croatian kuna + "CUP": 2, // Cuban peso + "CUC": 2, // Cuban convertible peso + "ANG": 2, // Netherlands Antillean guilder + "CZK": 2, // Czech koruna + "DKK": 2, // Danish krone + "DJF": 0, // Djiboutian franc + "DOP": 2, // Dominican peso + "EGP": 2, // Egyptian pound + "SVC": 2, // Salvadoran colón + "ERN": 2, // Eritrean nakfa + "SZL": 2, // Swazi lilangeni + "ETB": 2, // Ethiopian birr + "FKP": 2, // Falkland Islands pound + "FJD": 2, // Fiji dollar + "XPF": 0, // CFP franc + "GMD": 2, // Gambian dalasi + "GEL": 2, // Georgian lari + "GHS": 2, // Ghanaian cedi + "GIP": 2, // Gibraltar pound + "GTQ": 2, // Guatemalan quetzal + "GBP": 2, // Pound sterling + "GNF": 0, // Guinean franc + "GYD": 2, // Guyanese dollar + "HTG": 2, // Haitian gourde + "HNL": 2, // Honduran lempira + "HKD": 2, // Hong Kong dollar + "HUF": 2, // Hungarian forint + "ISK": 0, // Icelandic króna + "IDR": 2, // Indonesian rupiah + "IRR": 2, // Iranian rial + "IQD": 3, // Iraqi dinar + "ILS": 2, // Israeli new shekel + "JMD": 2, // Jamaican dollar + "JPY": 0, // Japanese yen + "JOD": 3, // Jordanian dinar + "KZT": 2, // Kazakhstani tenge + "KES": 2, // Kenyan shilling + "KPW": 2, // North Korean won + "KRW": 0, // South Korean won + "KWD": 3, // Kuwaiti dinar + "KGS": 2, // Kyrgyzstani som + "LAK": 2, // Lao kip + "LBP": 2, // Lebanese pound + "LSL": 2, // Lesotho loti + "ZAR": 2, // South African rand + "LRD": 2, // Liberian dollar + "LYD": 3, // Libyan dinar + "CHF": 2, // Swiss franc + "MOP": 2, // Macanese pataca + "MKD": 2, // Macedonian denar + "MGA": 2, // Malagasy ariary + "MWK": 2, // Malawian kwacha + "MYR": 2, // Malaysian ringgit + "MVR": 2, // Maldivian rufiyaa + "MRU": 2, // Mauritanian ouguiya + "MUR": 2, // Mauritian rupee + "MXN": 2, // Mexican peso + "MXV": 2, // Mexican Unidad de Inversion (UDI) (funds code) + "MDL": 2, // Moldovan leu + "MNT": 2, // Mongolian tögrög + "MAD": 2, // Moroccan dirham + "MZN": 2, // Mozambican metical + "MMK": 2, // Burmese kyat + "NAD": 2, // Namibian dollar + "NPR": 2, // Nepalese rupee + "NIO": 2, // Nicaraguan córdoba + "NGN": 2, // Nigerian naira + "OMR": 3, // Omani rial + "PKR": 2, // Pakistani rupee + "PAB": 2, // Panamanian balboa + "PGK": 2, // Papua New Guinean kina + "PYG": 0, // Paraguayan guaraní + "PEN": 2, // Peruvian sol + "PHP": 2, // Philippine peso + "PLN": 2, // Polish złoty + "QAR": 2, // Qatari riyal + "RON": 2, // Romanian leu + "RUB": 2, // Russian ruble + "RWF": 0, // Rwandan franc + "SHP": 2, // Saint Helena pound + "WST": 2, // Samoan tala + "STN": 2, // São Tomé and Príncipe dobra + "SAR": 2, // Saudi riyal + "RSD": 2, // Serbian dinar + "SCR": 2, // Seychelles rupee + "SLL": 2, // Sierra Leonean leone + "SGD": 2, // Singapore dollar + "SBD": 2, // Solomon Islands dollar + "SOS": 2, // Somali shilling + "SSP": 2, // South Sudanese pound + "LKR": 2, // Sri Lankan rupee + "SDG": 2, // Sudanese pound + "SRD": 2, // Surinamese dollar + "SEK": 2, // Swedish krona/kronor + "CHE": 2, // WIR Euro (complementary currency) + "CHW": 2, // WIR Franc (complementary currency) + "SYP": 2, // Syrian pound + "TWD": 2, // New Taiwan dollar + "TJS": 2, // Tajikistani somoni + "TZS": 2, // Tanzanian shilling + "THB": 2, // Thai baht + "TOP": 2, // Tongan paʻanga + "TTD": 2, // Trinidad and Tobago dollar + "TND": 3, // Tunisian dinar + "TRY": 2, // Turkish lira + "TMT": 2, // Turkmenistan manat + "UGX": 0, // Ugandan shilling + "UAH": 2, // Ukrainian hryvnia + "AED": 2, // United Arab Emirates dirham + "USN": 2, // United States dollar (next day) (funds code) + "UYU": 2, // Uruguayan peso + "UYI": 0, // Uruguay Peso en Unidades Indexadas (URUIURUI) (funds code) + "UYW": 4, // Unidad previsional[9] + "UZS": 2, // Uzbekistan som + "VUV": 0, // Vanuatu vatu + "VES": 2, // Venezuelan bolívar soberano + "VND": 0, // Vietnamese đồng + "YER": 2, // Yemeni rial + "ZMW": 2, // Zambian kwacha + "ZWL": 2, // Zimbabwean dollar A/10 } -} +) -func FormatAsset(cur string) models.Asset { +func FormatAsset(currencies map[string]int, cur string) models.Asset { asset := strings.ToUpper(string(cur)) - def, ok := currencies()[asset] + def, ok := currencies[asset] if !ok { return models.Asset(asset) } - if def.decimals == 0 { + if def == 0 { return models.Asset(asset) } - return models.Asset(fmt.Sprintf("%s/%d", asset, def.decimals)) + return models.Asset(fmt.Sprintf("%s/%d", asset, def)) } -func GetPrecision(cur string) (int, error) { +func GetPrecision(currencies map[string]int, cur string) (int, error) { asset := strings.ToUpper(string(cur)) - def, ok := currencies()[asset] + def, ok := currencies[asset] if !ok { return 0, errors.New("missing currencies") } - return def.decimals, nil + return def, nil } -func GetCurrencyAndPrecisionFromAsset(asset models.Asset) (string, int, error) { +func GetCurrencyAndPrecisionFromAsset(currencies map[string]int, asset models.Asset) (string, int, error) { parts := strings.Split(asset.String(), "/") if len(parts) != 2 { return "", 0, errors.New("invalid asset") } currency := parts[0] - precision, err := GetPrecision(currency) + precision, err := GetPrecision(currencies, currency) if err != nil { return "", 0, err } diff --git a/components/payments/cmd/connectors/internal/connectors/currencycloud/connector.go b/components/payments/cmd/connectors/internal/connectors/currencycloud/connector.go index 6a68fe7c96..e09a54b04b 100644 --- a/components/payments/cmd/connectors/internal/connectors/currencycloud/connector.go +++ b/components/payments/cmd/connectors/internal/connectors/currencycloud/connector.go @@ -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 users and transactions", @@ -77,7 +81,6 @@ func (c *Connector) Install(ctx task.ConnectorContext) error { } func (c *Connector) Uninstall(ctx context.Context) error { - return nil } diff --git a/components/payments/cmd/connectors/internal/connectors/currencycloud/currencies.go b/components/payments/cmd/connectors/internal/connectors/currencycloud/currencies.go new file mode 100644 index 0000000000..dd05ae31d3 --- /dev/null +++ b/components/payments/cmd/connectors/internal/connectors/currencycloud/currencies.go @@ -0,0 +1,51 @@ +package currencycloud + +import "github.com/formancehq/payments/cmd/connectors/internal/connectors/currency" + +var ( + // c.f.: https://support.currencycloud.com/hc/en-gb/articles/7840216562972-Currency-Decimal-Places + supportedCurrenciesWithDecimal = map[string]int{ + "AUD": currency.ISO4217Currencies["AUD"], // Australian Dollar + "CAD": currency.ISO4217Currencies["CAD"], // Canadian Dollar + "CZK": currency.ISO4217Currencies["CZK"], // Czech Koruna + "DKK": currency.ISO4217Currencies["DKK"], // Danish Krone + "EUR": currency.ISO4217Currencies["EUR"], // Euro + "HKD": currency.ISO4217Currencies["HKD"], // Hong Kong Dollar + "INR": currency.ISO4217Currencies["INR"], // Indian Rupee + "IDR": currency.ISO4217Currencies["IDR"], // Indonesia, Rupiah + "ILS": currency.ISO4217Currencies["ILS"], // New Israeli Shekel + "JPY": currency.ISO4217Currencies["JPY"], // Japan, Yen + "KES": currency.ISO4217Currencies["KES"], // Kenyan Shilling + "MYR": currency.ISO4217Currencies["MYR"], // Malaysian Ringgit + "MXN": currency.ISO4217Currencies["MXN"], // Mexican Peso + "NZD": currency.ISO4217Currencies["NZD"], // New Zealand Dollar + "NOK": currency.ISO4217Currencies["NOK"], // Norwegian Krone + "PHP": currency.ISO4217Currencies["PHP"], // Philippine Peso + "PLN": currency.ISO4217Currencies["PLN"], // Poland, Zloty + "RON": currency.ISO4217Currencies["RON"], // Romania, New Leu + "SAR": currency.ISO4217Currencies["SAR"], // Saudi Riyal + "SGD": currency.ISO4217Currencies["SGD"], // Singapore Dollar + "ZAR": currency.ISO4217Currencies["ZAR"], // South Africa, Rand + "SEK": currency.ISO4217Currencies["SEK"], // Swedish Krona + "CHF": currency.ISO4217Currencies["CHF"], // Swiss Franc + "THB": currency.ISO4217Currencies["THB"], // Thailand, Baht + "TRY": currency.ISO4217Currencies["TRY"], // New Turkish Lira + "GBP": currency.ISO4217Currencies["GBP"], // Pound Sterling + "AED": currency.ISO4217Currencies["AED"], // UAE Dirham + "USD": currency.ISO4217Currencies["USD"], // US Dollar + "UGX": currency.ISO4217Currencies["UGX"], // Uganda Shilling + "QAR": currency.ISO4217Currencies["QAR"], // Qatari Riyal + + // Unsupported currencies + // the following currencies are not existing in ISO 4217, so we prefer + // not to support them for now. + // "CNH": 2, // Chinese Yuan + + // The following currencies have a different value in ISO 4217, so we + // prefer to not support them for now. + // "HUF": 0, // Hungarian Forint + // "KWD": 2, // Kuwaiti Dinar + // "OMR": 2, // Rial Omani + // "BHD": 2, // Bahraini Dinar + } +) diff --git a/components/payments/cmd/connectors/internal/connectors/currencycloud/task_fetch_balances.go b/components/payments/cmd/connectors/internal/connectors/currencycloud/task_fetch_balances.go index 1bb468f263..051163aee7 100644 --- a/components/payments/cmd/connectors/internal/connectors/currencycloud/task_fetch_balances.go +++ b/components/payments/cmd/connectors/internal/connectors/currencycloud/task_fetch_balances.go @@ -3,9 +3,11 @@ package currencycloud import ( "context" "fmt" + "math" "math/big" "time" + "github.com/formancehq/payments/cmd/connectors/internal/connectors/currency" "github.com/formancehq/payments/cmd/connectors/internal/connectors/currencycloud/client" "github.com/formancehq/payments/cmd/connectors/internal/ingestion" "github.com/formancehq/payments/cmd/connectors/internal/metrics" @@ -70,6 +72,9 @@ func ingestBalancesBatch( ) error { batch := ingestion.BalanceBatch{} for _, balance := range 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.Amount) if !ok { @@ -77,7 +82,7 @@ func ingestBalancesBatch( } 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() batch = append(batch, &models.Balance{ @@ -85,7 +90,7 @@ func ingestBalancesBatch( Reference: balance.AccountID, ConnectorID: connectorID, }, - Asset: models.Asset(fmt.Sprintf("%s/2", balance.Currency)), + Asset: currency.FormatAsset(supportedCurrenciesWithDecimal, balance.Currency), Balance: &amountInt, CreatedAt: now, LastUpdatedAt: now, diff --git a/components/payments/cmd/connectors/internal/connectors/currencycloud/task_fetch_transactions.go b/components/payments/cmd/connectors/internal/connectors/currencycloud/task_fetch_transactions.go index 46e3dc86b2..854fb9be81 100644 --- a/components/payments/cmd/connectors/internal/connectors/currencycloud/task_fetch_transactions.go +++ b/components/payments/cmd/connectors/internal/connectors/currencycloud/task_fetch_transactions.go @@ -4,9 +4,11 @@ import ( "context" "encoding/json" "fmt" + "math" "math/big" "time" + "github.com/formancehq/payments/cmd/connectors/internal/connectors/currency" "github.com/formancehq/payments/cmd/connectors/internal/connectors/currencycloud/client" "github.com/formancehq/payments/cmd/connectors/internal/ingestion" "github.com/formancehq/payments/cmd/connectors/internal/metrics" @@ -32,8 +34,13 @@ func taskFetchTransactions(logger logging.Logger, client *client.Client, config } } -func ingestTransactions(ctx context.Context, logger logging.Logger, connectorID models.ConnectorID, - client *client.Client, ingester ingestion.Ingester, metricsRegistry metrics.MetricsRegistry, +func ingestTransactions( + ctx context.Context, + logger logging.Logger, + connectorID models.ConnectorID, + client *client.Client, + ingester ingestion.Ingester, + metricsRegistry metrics.MetricsRegistry, ) error { now := time.Now() defer func() { @@ -61,15 +68,21 @@ func ingestTransactions(ctx context.Context, logger logging.Logger, connectorID for _, transaction := range transactions { logger.Info(transaction) + precision, ok := supportedCurrenciesWithDecimal[transaction.Currency] + if !ok { + logger.Errorf("currency %s is not supported", transaction.Currency) + metricsRegistry.ConnectorCurrencyNotSupported().Add(ctx, 1, metric.WithAttributes(connectorAttrs...)) + continue + } + var amount big.Float - _, ok := amount.SetString(transaction.Amount) + _, ok = amount.SetString(transaction.Amount) if !ok { metricsRegistry.ConnectorObjectsErrors().Add(ctx, 1, paymentsAttrs) return fmt.Errorf("failed to parse amount %s", transaction.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) var rawData json.RawMessage @@ -96,7 +109,7 @@ func ingestTransactions(ctx context.Context, logger logging.Logger, connectorID Status: matchTransactionStatus(transaction.Status), Scheme: models.PaymentSchemeOther, Amount: &amountInt, - Asset: models.Asset(fmt.Sprintf("%s/2", transaction.Currency)), + Asset: currency.FormatAsset(supportedCurrenciesWithDecimal, transaction.Currency), RawData: rawData, }, } diff --git a/components/payments/cmd/connectors/internal/connectors/currencycloud/task_payments.go b/components/payments/cmd/connectors/internal/connectors/currencycloud/task_payments.go index e58024d205..5a5d3eb5bb 100644 --- a/components/payments/cmd/connectors/internal/connectors/currencycloud/task_payments.go +++ b/components/payments/cmd/connectors/internal/connectors/currencycloud/task_payments.go @@ -3,6 +3,7 @@ package currencycloud import ( "context" "fmt" + "math" "math/big" "time" @@ -83,13 +84,14 @@ func taskInitiatePayment(logger logging.Logger, currencyCloudClient *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 } transferAmount := big.NewFloat(0).SetInt(transfer.Amount) - transferAmount = transferAmount.Quo(transferAmount, big.NewFloat(100)) + transferAmount = transferAmount.Quo(transferAmount, big.NewFloat(math.Pow(10, float64(precision)))) amount, accuracy := transferAmount.Float64() if accuracy != big.Exact { return errors.New("amount is not accurate, psp does not support big ints") diff --git a/components/payments/cmd/connectors/internal/connectors/dummypay/connector.go b/components/payments/cmd/connectors/internal/connectors/dummypay/connector.go index 95ebff262d..574d5dbfb1 100644 --- a/components/payments/cmd/connectors/internal/connectors/dummypay/connector.go +++ b/components/payments/cmd/connectors/internal/connectors/dummypay/connector.go @@ -33,6 +33,10 @@ func (c *Connector) InitiatePayment(ctx task.ConnectorContext, transfer *models. return errors.New("not implemented") } +func (c *Connector) SupportedCurrenciesAndDecimals() map[string]int { + return supportedCurrenciesWithDecimal +} + // Install executes post-installation steps to read and generate files. // It is called after the connector is installed. func (c *Connector) Install(ctx task.ConnectorContext) error { diff --git a/components/payments/cmd/connectors/internal/connectors/dummypay/currencies.go b/components/payments/cmd/connectors/internal/connectors/dummypay/currencies.go new file mode 100644 index 0000000000..75b0ef3d35 --- /dev/null +++ b/components/payments/cmd/connectors/internal/connectors/dummypay/currencies.go @@ -0,0 +1,7 @@ +package dummypay + +import "github.com/formancehq/payments/cmd/connectors/internal/connectors/currency" + +var ( + supportedCurrenciesWithDecimal = currency.ISO4217Currencies +) diff --git a/components/payments/cmd/connectors/internal/connectors/mangopay/connector.go b/components/payments/cmd/connectors/internal/connectors/mangopay/connector.go index 3525bc6b91..b4819e0d38 100644 --- a/components/payments/cmd/connectors/internal/connectors/mangopay/connector.go +++ b/components/payments/cmd/connectors/internal/connectors/mangopay/connector.go @@ -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 users and transactions", diff --git a/components/payments/cmd/connectors/internal/connectors/mangopay/currencies.go b/components/payments/cmd/connectors/internal/connectors/mangopay/currencies.go new file mode 100644 index 0000000000..261e1b780c --- /dev/null +++ b/components/payments/cmd/connectors/internal/connectors/mangopay/currencies.go @@ -0,0 +1,24 @@ +package mangopay + +import "github.com/formancehq/payments/cmd/connectors/internal/connectors/currency" + +var ( + // c.f. https://mangopay.com/docs/api-basics/data-formats + 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 + "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 + "JPY": currency.ISO4217Currencies["JPY"], // Japan, Yen + "NOK": currency.ISO4217Currencies["NOK"], // Norwegian Krone + "PLN": currency.ISO4217Currencies["PLN"], // Poland, Zloty + "SEK": currency.ISO4217Currencies["SEK"], // Swedish Krona + "USD": currency.ISO4217Currencies["USD"], // US Dollar + "ZAR": currency.ISO4217Currencies["ZAR"], // South Africa, Rand + } +) diff --git a/components/payments/cmd/connectors/internal/connectors/mangopay/task_fetch_transactions.go b/components/payments/cmd/connectors/internal/connectors/mangopay/task_fetch_transactions.go index 1fca741be9..53d641fcbb 100644 --- a/components/payments/cmd/connectors/internal/connectors/mangopay/task_fetch_transactions.go +++ b/components/payments/cmd/connectors/internal/connectors/mangopay/task_fetch_transactions.go @@ -95,7 +95,7 @@ func ingestBatch( Type: paymentType, Status: matchPaymentStatus(payment.Status), Scheme: models.PaymentSchemeOther, - Asset: currency.FormatAsset(payment.DebitedFunds.Currency), + Asset: currency.FormatAsset(supportedCurrenciesWithDecimal, payment.DebitedFunds.Currency), RawData: rawData, }, } diff --git a/components/payments/cmd/connectors/internal/connectors/mangopay/task_fetch_wallets.go b/components/payments/cmd/connectors/internal/connectors/mangopay/task_fetch_wallets.go index b3f0e8de7b..5658291e57 100644 --- a/components/payments/cmd/connectors/internal/connectors/mangopay/task_fetch_wallets.go +++ b/components/payments/cmd/connectors/internal/connectors/mangopay/task_fetch_wallets.go @@ -79,7 +79,7 @@ func taskFetchWallets(logger logging.Logger, client *client.Client, userID strin CreatedAt: time.Unix(wallet.CreationDate, 0), Reference: wallet.ID, ConnectorID: connectorID, - DefaultAsset: currency.FormatAsset(wallet.Currency), + DefaultAsset: currency.FormatAsset(supportedCurrenciesWithDecimal, wallet.Currency), AccountName: wallet.Description, // Wallets are internal accounts on our side, since we // can have their balances. @@ -102,7 +102,7 @@ func taskFetchWallets(logger logging.Logger, client *client.Client, userID strin Reference: wallet.ID, ConnectorID: connectorID, }, - Asset: currency.FormatAsset(wallet.Balance.Currency), + Asset: currency.FormatAsset(supportedCurrenciesWithDecimal, wallet.Balance.Currency), Balance: &amount, CreatedAt: now, LastUpdatedAt: now, diff --git a/components/payments/cmd/connectors/internal/connectors/mangopay/task_payments.go b/components/payments/cmd/connectors/internal/connectors/mangopay/task_payments.go index d43ffd6a4a..99ccdcf018 100644 --- a/components/payments/cmd/connectors/internal/connectors/mangopay/task_payments.go +++ b/components/payments/cmd/connectors/internal/connectors/mangopay/task_payments.go @@ -89,8 +89,10 @@ func taskInitiatePayment(logger logging.Logger, mangopayClient *client.Client, t return err } + // No need to modify the amount since it's already in the correct format + // and precision (checked before during API call) var curr string - curr, _, err = currency.GetCurrencyAndPrecisionFromAsset(transfer.Asset) + curr, _, err = currency.GetCurrencyAndPrecisionFromAsset(supportedCurrenciesWithDecimal, transfer.Asset) if err != nil { return err } diff --git a/components/payments/cmd/connectors/internal/connectors/modulr/connector.go b/components/payments/cmd/connectors/internal/connectors/modulr/connector.go index 3cf8c135d7..886424d2dd 100644 --- a/components/payments/cmd/connectors/internal/connectors/modulr/connector.go +++ b/components/payments/cmd/connectors/internal/connectors/modulr/connector.go @@ -57,6 +57,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 accounts and transactions", diff --git a/components/payments/cmd/connectors/internal/connectors/modulr/currencies.go b/components/payments/cmd/connectors/internal/connectors/modulr/currencies.go new file mode 100644 index 0000000000..d54ca96535 --- /dev/null +++ b/components/payments/cmd/connectors/internal/connectors/modulr/currencies.go @@ -0,0 +1,20 @@ +package modulr + +import "github.com/formancehq/payments/cmd/connectors/internal/connectors/currency" + +var ( + // c.f. https://modulr.readme.io/docs/international-payments + supportedCurrenciesWithDecimal = map[string]int{ + "GBP": currency.ISO4217Currencies["GBP"], // Pound Sterling + "EUR": currency.ISO4217Currencies["EUR"], // Euro + "CZK": currency.ISO4217Currencies["CZK"], // Czech Koruna + "DKK": currency.ISO4217Currencies["DKK"], // Danish Krone + "NOK": currency.ISO4217Currencies["NOK"], // Norwegian Krone + "PLN": currency.ISO4217Currencies["PLN"], // Poland, Zloty + "SEK": currency.ISO4217Currencies["SEK"], // Swedish Krona + "CHF": currency.ISO4217Currencies["CHF"], // Swiss Franc + "USD": currency.ISO4217Currencies["USD"], // US Dollar + "HKD": currency.ISO4217Currencies["HKD"], // Hong Kong Dollar + "JPY": currency.ISO4217Currencies["JPY"], // Japan, Yen + } +) diff --git a/components/payments/cmd/connectors/internal/connectors/modulr/task_fetch_accounts.go b/components/payments/cmd/connectors/internal/connectors/modulr/task_fetch_accounts.go index 86cb4f3efa..1d86afd54e 100644 --- a/components/payments/cmd/connectors/internal/connectors/modulr/task_fetch_accounts.go +++ b/components/payments/cmd/connectors/internal/connectors/modulr/task_fetch_accounts.go @@ -5,6 +5,7 @@ import ( "encoding/json" "errors" "fmt" + "math" "math/big" "time" @@ -14,6 +15,7 @@ import ( "go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/metric" + "github.com/formancehq/payments/cmd/connectors/internal/connectors/currency" "github.com/formancehq/payments/cmd/connectors/internal/connectors/modulr/client" "github.com/formancehq/payments/cmd/connectors/internal/task" @@ -105,12 +107,16 @@ func ingestAccountsBatch( CreatedAt: openingDate, Reference: account.ID, ConnectorID: connectorID, - DefaultAsset: models.Asset(fmt.Sprintf("%s/2", account.Currency)), + DefaultAsset: currency.FormatAsset(supportedCurrenciesWithDecimal, account.Currency), AccountName: account.Name, Type: models.AccountTypeInternal, RawData: raw, }) + // No need to check if the currency is supported for accounts and + // balances. + precision, _ := supportedCurrenciesWithDecimal[account.Currency] + var amount big.Float _, ok := amount.SetString(account.Balance) if !ok { @@ -118,7 +124,7 @@ func ingestAccountsBatch( } var balance big.Int - amount.Mul(&amount, big.NewFloat(100)).Int(&balance) + amount.Mul(&amount, big.NewFloat(math.Pow(10, float64(precision)))).Int(&balance) now := time.Now() balancesBatch = append(balancesBatch, &models.Balance{ @@ -126,7 +132,7 @@ func ingestAccountsBatch( Reference: account.ID, ConnectorID: connectorID, }, - Asset: models.Asset(fmt.Sprintf("%s/2", account.Currency)), + Asset: currency.FormatAsset(supportedCurrenciesWithDecimal, account.Currency), Balance: &balance, CreatedAt: now, LastUpdatedAt: now, diff --git a/components/payments/cmd/connectors/internal/connectors/modulr/task_fetch_transactions.go b/components/payments/cmd/connectors/internal/connectors/modulr/task_fetch_transactions.go index 3187f84f40..709f857b82 100644 --- a/components/payments/cmd/connectors/internal/connectors/modulr/task_fetch_transactions.go +++ b/components/payments/cmd/connectors/internal/connectors/modulr/task_fetch_transactions.go @@ -4,10 +4,12 @@ import ( "context" "encoding/json" "fmt" + "math" "math/big" "strings" "time" + "github.com/formancehq/payments/cmd/connectors/internal/connectors/currency" "github.com/formancehq/payments/cmd/connectors/internal/connectors/modulr/client" "github.com/formancehq/payments/cmd/connectors/internal/ingestion" "github.com/formancehq/payments/cmd/connectors/internal/metrics" @@ -25,6 +27,7 @@ var ( func taskFetchTransactions(logger logging.Logger, client *client.Client, accountID string) task.Task { return func( ctx context.Context, + logger logging.Logger, connectorID models.ConnectorID, ingester ingestion.Ingester, metricsRegistry metrics.MetricsRegistry, @@ -54,14 +57,21 @@ func taskFetchTransactions(logger logging.Logger, client *client.Client, account paymentType := matchTransactionType(transaction.Type) + precision, ok := supportedCurrenciesWithDecimal[transaction.Account.Currency] + if !ok { + logger.Errorf("currency %s is not supported", transaction.Account.Currency) + metricsRegistry.ConnectorCurrencyNotSupported().Add(ctx, 1, metric.WithAttributes(connectorAttrs...)) + continue + } + var amount big.Float - _, ok := amount.SetString(transaction.Amount.String()) + _, ok = amount.SetString(transaction.Amount.String()) if !ok { return fmt.Errorf("failed to parse amount %s", transaction.Amount.String()) } 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{ @@ -78,7 +88,7 @@ func taskFetchTransactions(logger logging.Logger, client *client.Client, account Status: models.PaymentStatusSucceeded, Scheme: models.PaymentSchemeOther, Amount: &amountInt, - Asset: models.Asset(fmt.Sprintf("%s/2", transaction.Account.Currency)), + Asset: currency.FormatAsset(supportedCurrenciesWithDecimal, transaction.Account.Currency), RawData: rawData, }, } diff --git a/components/payments/cmd/connectors/internal/connectors/modulr/task_payments.go b/components/payments/cmd/connectors/internal/connectors/modulr/task_payments.go index a7307f4963..44120b3cea 100644 --- a/components/payments/cmd/connectors/internal/connectors/modulr/task_payments.go +++ b/components/payments/cmd/connectors/internal/connectors/modulr/task_payments.go @@ -2,6 +2,7 @@ package modulr import ( "context" + "math" "math/big" "regexp" "time" @@ -84,13 +85,14 @@ func taskInitiatePayment(logger logging.Logger, modulrClient *client.Client, tra } 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)))) description := "" if len(transfer.Description) <= 18 && ReferencePatternRegexp.MatchString(transfer.Description) { diff --git a/components/payments/cmd/connectors/internal/connectors/moneycorp/connector.go b/components/payments/cmd/connectors/internal/connectors/moneycorp/connector.go index 8c84b080e1..bf57f11ee7 100644 --- a/components/payments/cmd/connectors/internal/connectors/moneycorp/connector.go +++ b/components/payments/cmd/connectors/internal/connectors/moneycorp/connector.go @@ -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 accounts and transactions", diff --git a/components/payments/cmd/connectors/internal/connectors/moneycorp/currencies.go b/components/payments/cmd/connectors/internal/connectors/moneycorp/currencies.go new file mode 100644 index 0000000000..f0bff20789 --- /dev/null +++ b/components/payments/cmd/connectors/internal/connectors/moneycorp/currencies.go @@ -0,0 +1,63 @@ +package moneycorp + +import "github.com/formancehq/payments/cmd/connectors/internal/connectors/currency" + +var ( + supportedCurrenciesWithDecimal = map[string]int{ + "AED": currency.ISO4217Currencies["AED"], // UAE Dirham + "AUD": currency.ISO4217Currencies["AUD"], // Australian Dollar + "BBD": currency.ISO4217Currencies["BBD"], // Barbados Dollar + "BGN": currency.ISO4217Currencies["BGN"], // Bulgarian lev + "BHD": currency.ISO4217Currencies["BHD"], // Bahraini dinar + "BWP": currency.ISO4217Currencies["BWP"], // Botswana pula + "CAD": currency.ISO4217Currencies["CAD"], // Canadian dollar + "CHF": currency.ISO4217Currencies["CHF"], // Swiss franc + "CZK": currency.ISO4217Currencies["CZK"], // Czech koruna + "DKK": currency.ISO4217Currencies["DKK"], // Danish krone + "EUR": currency.ISO4217Currencies["EUR"], // Euro + "GBP": currency.ISO4217Currencies["GBP"], // Pound sterling + "GHS": currency.ISO4217Currencies["GHS"], // Ghanaian cedi + "HKD": currency.ISO4217Currencies["HKD"], // Hong Kong dollar + "ILS": currency.ISO4217Currencies["ILS"], // Israeli new shekel + "INR": currency.ISO4217Currencies["INR"], // Indian rupee + "JMD": currency.ISO4217Currencies["JMD"], // Jamaican dollar + "JPY": currency.ISO4217Currencies["JPY"], // Japanese yen + "KES": currency.ISO4217Currencies["KES"], // Kenyan shilling + "LKR": currency.ISO4217Currencies["LKR"], // Sri Lankan rupee + "MAD": currency.ISO4217Currencies["MAD"], // Moroccan dirham + "MUR": currency.ISO4217Currencies["MUR"], // Mauritian rupee + "MXN": currency.ISO4217Currencies["MXN"], // Mexican peso + "NOK": currency.ISO4217Currencies["NOK"], // Norwegian krone + "NPR": currency.ISO4217Currencies["NPR"], // Nepalese rupee + "NZD": currency.ISO4217Currencies["NZD"], // New Zealand dollar + "OMR": currency.ISO4217Currencies["OMR"], // Omani rial + "PHP": currency.ISO4217Currencies["PHP"], // Philippine peso + "PKR": currency.ISO4217Currencies["PKR"], // Pakistani rupee + "PLN": currency.ISO4217Currencies["PLN"], // Polish złoty + "QAR": currency.ISO4217Currencies["QAR"], // Qatari riyal + "RON": currency.ISO4217Currencies["RON"], // Romanian leu + "RSD": currency.ISO4217Currencies["RSD"], // Serbian dinar + "SAR": currency.ISO4217Currencies["SAR"], // Saudi riyal + "SEK": currency.ISO4217Currencies["SEK"], // Swedish krona/kronor + "SGD": currency.ISO4217Currencies["SGD"], // Singapore dollar + "THB": currency.ISO4217Currencies["THB"], // Thai baht + "TRY": currency.ISO4217Currencies["TRY"], // Turkish lira + "TTD": currency.ISO4217Currencies["TTD"], // Trinidad and Tobago dollar + "UGX": currency.ISO4217Currencies["UGX"], // Ugandan shilling + "USD": currency.ISO4217Currencies["USD"], // United States dollar + "XCD": currency.ISO4217Currencies["XCD"], // East Caribbean dollar + "ZAR": currency.ISO4217Currencies["ZAR"], // South African rand + "ZMW": currency.ISO4217Currencies["ZMW"], // Zambian kwacha + + // All following currencies have not the same decimals given by + // Moneycorp compared to the ISO 4217 standard. + // Let's not handle them for now. + // "CNH": 2, // Chinese Yuan + // "IDR": 2, // Indonesian rupiah + // "ISK": 0, // Icelandic króna + // "HUF": 2, // Hungarian forint + // "JOD": 3, // Jordanian dinar + // "KWD": 3, // Kuwaiti dinar + // "TND": 3, // Tunisian dinar + } +) diff --git a/components/payments/cmd/connectors/internal/connectors/moneycorp/task_fetch_balances.go b/components/payments/cmd/connectors/internal/connectors/moneycorp/task_fetch_balances.go index 0030c776b2..69bc914b5e 100644 --- a/components/payments/cmd/connectors/internal/connectors/moneycorp/task_fetch_balances.go +++ b/components/payments/cmd/connectors/internal/connectors/moneycorp/task_fetch_balances.go @@ -67,7 +67,7 @@ func ingestBalancesBatch( return fmt.Errorf("failed to parse amount %s", balance.Attributes.AvailableBalance.String()) } - precision, err := currency.GetPrecision(balance.Attributes.CurrencyCode) + precision, err := currency.GetPrecision(supportedCurrenciesWithDecimal, balance.Attributes.CurrencyCode) if err != nil { return err } @@ -81,7 +81,7 @@ func ingestBalancesBatch( Reference: accountID, ConnectorID: connectorID, }, - Asset: currency.FormatAsset(balance.Attributes.CurrencyCode), + Asset: currency.FormatAsset(supportedCurrenciesWithDecimal, balance.Attributes.CurrencyCode), Balance: &amountInt, CreatedAt: now, LastUpdatedAt: now, diff --git a/components/payments/cmd/connectors/internal/connectors/moneycorp/task_fetch_recipients.go b/components/payments/cmd/connectors/internal/connectors/moneycorp/task_fetch_recipients.go index 349d0a9728..06ab3bbc83 100644 --- a/components/payments/cmd/connectors/internal/connectors/moneycorp/task_fetch_recipients.go +++ b/components/payments/cmd/connectors/internal/connectors/moneycorp/task_fetch_recipients.go @@ -89,7 +89,7 @@ func ingestRecipientsBatch( CreatedAt: createdAt, Reference: recipient.ID, ConnectorID: connectorID, - DefaultAsset: currency.FormatAsset(recipient.Attributes.BankAccountCurrency), + DefaultAsset: currency.FormatAsset(supportedCurrenciesWithDecimal, recipient.Attributes.BankAccountCurrency), AccountName: recipient.Attributes.BankAccountName, Type: models.AccountTypeExternal, RawData: raw, diff --git a/components/payments/cmd/connectors/internal/connectors/moneycorp/task_fetch_transactions.go b/components/payments/cmd/connectors/internal/connectors/moneycorp/task_fetch_transactions.go index 0d1d602ea1..6fe94698bd 100644 --- a/components/payments/cmd/connectors/internal/connectors/moneycorp/task_fetch_transactions.go +++ b/components/payments/cmd/connectors/internal/connectors/moneycorp/task_fetch_transactions.go @@ -97,7 +97,7 @@ func ingestBatch( return fmt.Errorf("failed to parse amount %s", transaction.Attributes.Amount.String()) } - c, err := currency.GetPrecision(transaction.Attributes.Currency) + c, err := currency.GetPrecision(supportedCurrenciesWithDecimal, transaction.Attributes.Currency) if err != nil { return err } @@ -118,7 +118,7 @@ func ingestBatch( Reference: transaction.ID, ConnectorID: connectorID, Amount: &amountInt, - Asset: currency.FormatAsset(transaction.Attributes.Currency), + Asset: currency.FormatAsset(supportedCurrenciesWithDecimal, transaction.Attributes.Currency), Type: paymentType, Status: models.PaymentStatusSucceeded, Scheme: models.PaymentSchemeOther, diff --git a/components/payments/cmd/connectors/internal/connectors/moneycorp/task_payments.go b/components/payments/cmd/connectors/internal/connectors/moneycorp/task_payments.go index 95092e8d7c..b060e808cd 100644 --- a/components/payments/cmd/connectors/internal/connectors/moneycorp/task_payments.go +++ b/components/payments/cmd/connectors/internal/connectors/moneycorp/task_payments.go @@ -84,7 +84,7 @@ func taskInitiatePayment(logger logging.Logger, moneycorpClient *client.Client, var curr string var precision int - curr, precision, err = currency.GetCurrencyAndPrecisionFromAsset(transfer.Asset) + curr, precision, err = currency.GetCurrencyAndPrecisionFromAsset(supportedCurrenciesWithDecimal, transfer.Asset) if err != nil { return err } diff --git a/components/payments/cmd/connectors/internal/connectors/stripe/connector.go b/components/payments/cmd/connectors/internal/connectors/stripe/connector.go index ae9836929d..3298d0158d 100644 --- a/components/payments/cmd/connectors/internal/connectors/stripe/connector.go +++ b/components/payments/cmd/connectors/internal/connectors/stripe/connector.go @@ -43,6 +43,10 @@ func (c *Connector) Install(ctx task.ConnectorContext) error { }) } +func (c *Connector) SupportedCurrenciesAndDecimals() map[string]int { + return supportedCurrenciesWithDecimal +} + func (c *Connector) Uninstall(ctx context.Context) error { return nil } diff --git a/components/payments/cmd/connectors/internal/connectors/stripe/currencies.go b/components/payments/cmd/connectors/internal/connectors/stripe/currencies.go new file mode 100644 index 0000000000..4e6518ec52 --- /dev/null +++ b/components/payments/cmd/connectors/internal/connectors/stripe/currencies.go @@ -0,0 +1,162 @@ +package stripe + +import "github.com/formancehq/payments/cmd/connectors/internal/connectors/currency" + +var ( + // c.f. https://stripe.com/docs/currencies#zero-decimal + supportedCurrenciesWithDecimal = map[string]int{ + "USD": currency.ISO4217Currencies["USD"], // United States dollar + "AED": currency.ISO4217Currencies["AED"], // United Arab Emirates dirham + "AFN": currency.ISO4217Currencies["AFN"], // Afghan afghani + "ALL": currency.ISO4217Currencies["ALL"], // Albanian lek + "AMD": currency.ISO4217Currencies["AMD"], // Armenian dram + "ANG": currency.ISO4217Currencies["ANG"], // Netherlands Antillean guilder + "AOA": currency.ISO4217Currencies["AOA"], // Angolan kwanza + "ARS": currency.ISO4217Currencies["ARS"], // Argentine peso + "AUD": currency.ISO4217Currencies["AUD"], // Australian dollar + "AWG": currency.ISO4217Currencies["AWG"], // Aruban florin + "AZN": currency.ISO4217Currencies["AZN"], // Azerbaijani manat + "BAM": currency.ISO4217Currencies["BAM"], // Bosnia and Herzegovina convertible mark + "BBD": currency.ISO4217Currencies["BBD"], // Barbados dollar + "BDT": currency.ISO4217Currencies["BDT"], // Bangladeshi taka + "BGN": currency.ISO4217Currencies["BGN"], // Bulgarian lev + "BIF": currency.ISO4217Currencies["BIF"], // Burundian franc + "BMD": currency.ISO4217Currencies["BMD"], // Bermudian dollar + "BND": currency.ISO4217Currencies["BND"], // Brunei dollar + "BOB": currency.ISO4217Currencies["BOB"], // Bolivian boliviano + "BRL": currency.ISO4217Currencies["BRL"], // Brazilian real + "BSD": currency.ISO4217Currencies["BSD"], // Bahamian dollar + "BWP": currency.ISO4217Currencies["BWP"], // Botswana pula + "BYN": currency.ISO4217Currencies["BYN"], // Belarusian ruble + "BZD": currency.ISO4217Currencies["BZD"], // Belize dollar + "CAD": currency.ISO4217Currencies["CAD"], // Canadian dollar + "CDF": currency.ISO4217Currencies["CDF"], // Congolese franc + "CHF": currency.ISO4217Currencies["CHF"], // Swiss franc + "CLP": currency.ISO4217Currencies["CLP"], // Chilean peso + "CNY": currency.ISO4217Currencies["CNY"], // Chinese yuan + "COP": currency.ISO4217Currencies["COP"], // Colombian peso + "CRC": currency.ISO4217Currencies["CRC"], // Costa Rican colon + "CVE": currency.ISO4217Currencies["CVE"], // Cape Verdean escudo + "CZK": currency.ISO4217Currencies["CZK"], // Czech koruna + "DJF": currency.ISO4217Currencies["DJF"], // Djiboutian franc + "DKK": currency.ISO4217Currencies["DKK"], // Danish krone + "DOP": currency.ISO4217Currencies["DOP"], // Dominican peso + "DZD": currency.ISO4217Currencies["DZD"], // Algerian dinar + "EGP": currency.ISO4217Currencies["EGP"], // Egyptian pound + "ETB": currency.ISO4217Currencies["ETB"], // Ethiopian birr + "EUR": currency.ISO4217Currencies["EUR"], // Euro + "FJD": currency.ISO4217Currencies["FJD"], // Fiji dollar + "FKP": currency.ISO4217Currencies["FKP"], // Falkland Islands pound + "GBP": currency.ISO4217Currencies["GBP"], // Pound sterling + "GEL": currency.ISO4217Currencies["GEL"], // Georgian lari + "GIP": currency.ISO4217Currencies["GIP"], // Gibraltar pound + "GMD": currency.ISO4217Currencies["GMD"], // Gambian dalasi + "GNF": currency.ISO4217Currencies["GNF"], // Guinean franc + "GTQ": currency.ISO4217Currencies["GTQ"], // Guatemalan quetzal + "GYD": currency.ISO4217Currencies["GYD"], // Guyanese dollar + "HKD": currency.ISO4217Currencies["HKD"], // Hong Kong dollar + "HNL": currency.ISO4217Currencies["HNL"], // Honduran lempira + "HTG": currency.ISO4217Currencies["HTG"], // Haitian gourde + "IDR": currency.ISO4217Currencies["IDR"], // Indonesian rupiah + "ILS": currency.ISO4217Currencies["ILS"], // Israeli new shekel + "INR": currency.ISO4217Currencies["INR"], // Indian rupee + "JMD": currency.ISO4217Currencies["JMD"], // Jamaican dollar + "JPY": currency.ISO4217Currencies["JPY"], // Japanese yen + "KES": currency.ISO4217Currencies["KES"], // Kenyan shilling + "KGS": currency.ISO4217Currencies["KGS"], // Kyrgyzstani som + "KHR": currency.ISO4217Currencies["KHR"], // Cambodian riel + "KMF": currency.ISO4217Currencies["KMF"], // Comoro franc + "KRW": currency.ISO4217Currencies["KRW"], // South Korean won + "KYD": currency.ISO4217Currencies["KYD"], // Cayman Islands dollar + "KZT": currency.ISO4217Currencies["KZT"], // Kazakhstani tenge + "LAK": currency.ISO4217Currencies["LAK"], // Lao kip + "LBP": currency.ISO4217Currencies["LBP"], // Lebanese pound + "LKR": currency.ISO4217Currencies["LKR"], // Sri Lankan rupee + "LRD": currency.ISO4217Currencies["LRD"], // Liberian dollar + "LSL": currency.ISO4217Currencies["LSL"], // Lesotho loti + "MAD": currency.ISO4217Currencies["MAD"], // Moroccan dirham + "MDL": currency.ISO4217Currencies["MDL"], // Moldovan leu + "MKD": currency.ISO4217Currencies["MKD"], // Macedonian denar + "MMK": currency.ISO4217Currencies["MMK"], // Burmese kyat + "MNT": currency.ISO4217Currencies["MNT"], // Mongolian tögrög + "MOP": currency.ISO4217Currencies["MOP"], // Macanese pataca + "MUR": currency.ISO4217Currencies["MUR"], // Mauritian rupee + "MVR": currency.ISO4217Currencies["MVR"], // Maldivian rufiyaa + "MWK": currency.ISO4217Currencies["MWK"], // Malawian kwacha + "MXN": currency.ISO4217Currencies["MXN"], // Mexican peso + "MYR": currency.ISO4217Currencies["MYR"], // Malaysian ringgit + "MZN": currency.ISO4217Currencies["MZN"], // Mozambican metical + "NAD": currency.ISO4217Currencies["NAD"], // Namibian dollar + "NGN": currency.ISO4217Currencies["NGN"], // Nigerian naira + "NIO": currency.ISO4217Currencies["NIO"], // Nicaraguan córdoba + "NOK": currency.ISO4217Currencies["NOK"], // Norwegian krone + "NPR": currency.ISO4217Currencies["NPR"], // Nepalese rupee + "NZD": currency.ISO4217Currencies["NZD"], // New Zealand dollar + "PAB": currency.ISO4217Currencies["PAB"], // Panamanian balboa + "PEN": currency.ISO4217Currencies["PEN"], // Peruvian sol + "PGK": currency.ISO4217Currencies["PGK"], // Papua New Guinean kina + "PHP": currency.ISO4217Currencies["PHP"], // Philippine peso + "PKR": currency.ISO4217Currencies["PKR"], // Pakistani rupee + "PLN": currency.ISO4217Currencies["PLN"], // Polish złoty + "PYG": currency.ISO4217Currencies["PYG"], // Paraguayan guaraní + "QAR": currency.ISO4217Currencies["QAR"], // Qatari riyal + "RON": currency.ISO4217Currencies["RON"], // Romanian leu + "RSD": currency.ISO4217Currencies["RSD"], // Serbian dinar + "RUB": currency.ISO4217Currencies["RUB"], // Russian ruble + "RWF": currency.ISO4217Currencies["RWF"], // Rwandan franc + "SAR": currency.ISO4217Currencies["SAR"], // Saudi riyal + "SBD": currency.ISO4217Currencies["SBD"], // Solomon Islands dollar + "SCR": currency.ISO4217Currencies["SCR"], // Seychelles rupee + "SEK": currency.ISO4217Currencies["SEK"], // Swedish krona/kronor + "SGD": currency.ISO4217Currencies["SGD"], // Singapore dollar + "SHP": currency.ISO4217Currencies["SHP"], // Saint Helena pound + "SOS": currency.ISO4217Currencies["SOS"], // Somali shilling + "SRD": currency.ISO4217Currencies["SRD"], // Surinamese dollar + "SZL": currency.ISO4217Currencies["SZL"], // Swazi lilangeni + "THB": currency.ISO4217Currencies["THB"], // Thai baht + "TJS": currency.ISO4217Currencies["TJS"], // Tajikistani somoni + "TOP": currency.ISO4217Currencies["TOP"], // Tongan paʻanga + "TRY": currency.ISO4217Currencies["TRY"], // Turkish lira + "TTD": currency.ISO4217Currencies["TTD"], // Trinidad and Tobago dollar + "TZS": currency.ISO4217Currencies["TZS"], // Tanzanian shilling + "UAH": currency.ISO4217Currencies["UAH"], // Ukrainian hryvnia + "UYU": currency.ISO4217Currencies["UYU"], // Uruguayan peso + "UZS": currency.ISO4217Currencies["UZS"], // Uzbekistan som + "VND": currency.ISO4217Currencies["VND"], // Vietnamese đồng + "VUV": currency.ISO4217Currencies["VUV"], // Vanuatu vatu + "WST": currency.ISO4217Currencies["WST"], // Samoan tala + "XAF": currency.ISO4217Currencies["XAF"], // Central African CFA franc + "XCD": currency.ISO4217Currencies["XCD"], // East Caribbean dollar + "XOF": currency.ISO4217Currencies["XOF"], // West African CFA franc + "XPF": currency.ISO4217Currencies["XPF"], // CFP franc + "YER": currency.ISO4217Currencies["YER"], // Yemeni rial + "ZAR": currency.ISO4217Currencies["ZAR"], // South African rand + "ZMW": currency.ISO4217Currencies["ZMW"], // Zambian kwacha + + // Unsupported currencies + // The following currencies are not in the ISO 4217 standard, + //so let's not handle them for now. + // "SLE": 2 // Sierra Leonean leone + // "STD": 2 // São Tomé and Príncipe dobra + + // The following currencies have not the same decimals in Stripe compared + // to ISO 4217 standard, so let's not handle them for now. + // "MGA": 2, // Malagasy ariary + + // The following currencies are 3 decimals currencies, but in order + // to use them with Stripe, it requires the last digit to be 0. + // Let's not handle them for now. + // "BHD": 3, // Bahraini dinar + // "JOD": 3, // Jordanian dinar + // "KWD": 3, // Kuwaiti dinar + // "OMR": 3, // Omani rial + // "TND": 3, // Tunisian dinar + + // The following currencies are apecial cases in stripe API (cf link above) + // let's not handle them for now. + // "ISK": 0, // Icelandic króna + // "HUF": 2, // Hungarian forint + // "UGX": 0, // Ugandan shilling + // "TWD": 2 // New Taiwan dollar + } +) diff --git a/components/payments/cmd/connectors/internal/connectors/stripe/task_fetch_accounts.go b/components/payments/cmd/connectors/internal/connectors/stripe/task_fetch_accounts.go index 84f92d4500..334efd01e9 100644 --- a/components/payments/cmd/connectors/internal/connectors/stripe/task_fetch_accounts.go +++ b/components/payments/cmd/connectors/internal/connectors/stripe/task_fetch_accounts.go @@ -145,7 +145,7 @@ func ingestAccountsBatch( CreatedAt: time.Unix(account.Created, 0), Reference: account.ID, ConnectorID: connectorID, - DefaultAsset: currency.FormatAsset(string(account.DefaultCurrency)), + DefaultAsset: currency.FormatAsset(supportedCurrenciesWithDecimal, string(account.DefaultCurrency)), Type: models.AccountTypeInternal, RawData: raw, }) diff --git a/components/payments/cmd/connectors/internal/connectors/stripe/task_fetch_balances.go b/components/payments/cmd/connectors/internal/connectors/stripe/task_fetch_balances.go index db622bf100..724cc97c19 100644 --- a/components/payments/cmd/connectors/internal/connectors/stripe/task_fetch_balances.go +++ b/components/payments/cmd/connectors/internal/connectors/stripe/task_fetch_balances.go @@ -46,7 +46,7 @@ func balancesTask(account string, client *client.DefaultClient) func(ctx context Reference: account, ConnectorID: connectorID, }, - Asset: currency.FormatAsset(string(balance.Currency)), + Asset: currency.FormatAsset(supportedCurrenciesWithDecimal, string(balance.Currency)), Balance: big.NewInt(balance.Value), CreatedAt: timestamp, LastUpdatedAt: timestamp, diff --git a/components/payments/cmd/connectors/internal/connectors/stripe/task_fetch_external_accounts.go b/components/payments/cmd/connectors/internal/connectors/stripe/task_fetch_external_accounts.go index a254293ccd..2ff0e3e6fc 100644 --- a/components/payments/cmd/connectors/internal/connectors/stripe/task_fetch_external_accounts.go +++ b/components/payments/cmd/connectors/internal/connectors/stripe/task_fetch_external_accounts.go @@ -89,7 +89,7 @@ func ingestExternalAccountsBatch( CreatedAt: time.Unix(account.BankAccount.Account.Created, 0), Reference: account.ID, ConnectorID: connectorID, - DefaultAsset: currency.FormatAsset(string(account.BankAccount.Account.DefaultCurrency)), + DefaultAsset: currency.FormatAsset(supportedCurrenciesWithDecimal, string(account.BankAccount.Account.DefaultCurrency)), Type: models.AccountTypeExternal, RawData: raw, }) diff --git a/components/payments/cmd/connectors/internal/connectors/stripe/task_payments.go b/components/payments/cmd/connectors/internal/connectors/stripe/task_payments.go index 935011bfc1..8332de5bfc 100644 --- a/components/payments/cmd/connectors/internal/connectors/stripe/task_payments.go +++ b/components/payments/cmd/connectors/internal/connectors/stripe/task_payments.go @@ -81,7 +81,7 @@ func initiatePaymentTask(transferID string, stripeClient *client.DefaultClient) } var curr string - curr, _, err = currency.GetCurrencyAndPrecisionFromAsset(transfer.Asset) + curr, _, err = currency.GetCurrencyAndPrecisionFromAsset(supportedCurrenciesWithDecimal, transfer.Asset) if err != nil { return err } diff --git a/components/payments/cmd/connectors/internal/connectors/stripe/translate.go b/components/payments/cmd/connectors/internal/connectors/stripe/translate.go index 554066b733..76baf21f59 100644 --- a/components/payments/cmd/connectors/internal/connectors/stripe/translate.go +++ b/components/payments/cmd/connectors/internal/connectors/stripe/translate.go @@ -49,6 +49,11 @@ func createBatchElement( switch balanceTransaction.Type { case stripe.BalanceTransactionTypeCharge: + _, ok := supportedCurrenciesWithDecimal[string(balanceTransaction.Source.Charge.Currency)] + if !ok { + return ingestion.PaymentBatchElement{}, false + } + payment = models.Payment{ ID: models.PaymentID{ PaymentReference: models.PaymentReference{ @@ -62,7 +67,7 @@ func createBatchElement( Type: models.PaymentTypePayIn, Status: models.PaymentStatusSucceeded, Amount: big.NewInt(balanceTransaction.Source.Charge.Amount), - Asset: currency.FormatAsset(string(balanceTransaction.Source.Charge.Currency)), + Asset: currency.FormatAsset(supportedCurrenciesWithDecimal, string(balanceTransaction.Source.Charge.Currency)), RawData: rawData, Scheme: models.PaymentScheme(balanceTransaction.Source.Charge.PaymentMethodDetails.Card.Brand), CreatedAt: time.Unix(balanceTransaction.Created, 0), @@ -76,6 +81,11 @@ func createBatchElement( } case stripe.BalanceTransactionTypePayout: + _, ok := supportedCurrenciesWithDecimal[string(balanceTransaction.Source.Payout.Currency)] + if !ok { + return ingestion.PaymentBatchElement{}, false + } + payment = models.Payment{ ID: models.PaymentID{ PaymentReference: models.PaymentReference{ @@ -90,7 +100,7 @@ func createBatchElement( Status: convertPayoutStatus(balanceTransaction.Source.Payout.Status), Amount: big.NewInt(balanceTransaction.Source.Payout.Amount), RawData: rawData, - Asset: currency.FormatAsset(string(balanceTransaction.Source.Payout.Currency)), + Asset: currency.FormatAsset(supportedCurrenciesWithDecimal, string(balanceTransaction.Source.Payout.Currency)), Scheme: func() models.PaymentScheme { switch balanceTransaction.Source.Payout.Type { case stripe.PayoutTypeBank: @@ -112,6 +122,11 @@ func createBatchElement( } case stripe.BalanceTransactionTypeTransfer: + _, ok := supportedCurrenciesWithDecimal[string(balanceTransaction.Source.Transfer.Currency)] + if !ok { + return ingestion.PaymentBatchElement{}, false + } + payment = models.Payment{ ID: models.PaymentID{ PaymentReference: models.PaymentReference{ @@ -126,7 +141,7 @@ func createBatchElement( Status: models.PaymentStatusSucceeded, Amount: big.NewInt(balanceTransaction.Source.Transfer.Amount), RawData: rawData, - Asset: currency.FormatAsset(string(balanceTransaction.Source.Transfer.Currency)), + Asset: currency.FormatAsset(supportedCurrenciesWithDecimal, string(balanceTransaction.Source.Transfer.Currency)), Scheme: models.PaymentSchemeOther, CreatedAt: time.Unix(balanceTransaction.Created, 0), } @@ -169,6 +184,11 @@ func createBatchElement( } case stripe.BalanceTransactionTypePayment: + _, ok := supportedCurrenciesWithDecimal[string(balanceTransaction.Source.Charge.Currency)] + if !ok { + return ingestion.PaymentBatchElement{}, false + } + payment = models.Payment{ ID: models.PaymentID{ PaymentReference: models.PaymentReference{ @@ -183,7 +203,7 @@ func createBatchElement( Status: models.PaymentStatusSucceeded, Amount: big.NewInt(balanceTransaction.Source.Charge.Amount), RawData: rawData, - Asset: currency.FormatAsset(string(balanceTransaction.Source.Charge.Currency)), + Asset: currency.FormatAsset(supportedCurrenciesWithDecimal, string(balanceTransaction.Source.Charge.Currency)), Scheme: models.PaymentSchemeOther, CreatedAt: time.Unix(balanceTransaction.Created, 0), } diff --git a/components/payments/cmd/connectors/internal/connectors/wise/connector.go b/components/payments/cmd/connectors/internal/connectors/wise/connector.go index 310e9c1d6b..a4f2355616 100644 --- a/components/payments/cmd/connectors/internal/connectors/wise/connector.go +++ b/components/payments/cmd/connectors/internal/connectors/wise/connector.go @@ -57,6 +57,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 { descriptor, err := models.EncodeTaskDescriptor(TaskDescriptor{ Name: "Fetch profiles from client", diff --git a/components/payments/cmd/connectors/internal/connectors/wise/currencies.go b/components/payments/cmd/connectors/internal/connectors/wise/currencies.go new file mode 100644 index 0000000000..ad2f38fbc3 --- /dev/null +++ b/components/payments/cmd/connectors/internal/connectors/wise/currencies.go @@ -0,0 +1,33 @@ +package wise + +import "github.com/formancehq/payments/cmd/connectors/internal/connectors/currency" + +var ( + // c.f. https://wise.com/help/articles/2897238/which-currencies-can-i-add-keep-and-receive-in-my-wise-account + supportedCurrenciesWithDecimal = map[string]int{ + "AUD": currency.ISO4217Currencies["AUD"], // Australian dollar + "BGN": currency.ISO4217Currencies["BGN"], // Bulgarian lev + "BRL": currency.ISO4217Currencies["BRL"], // Brazilian real + "CAD": currency.ISO4217Currencies["CAD"], // Canadian dollar + "CNY": currency.ISO4217Currencies["CNY"], // Chinese yuan + "CHF": currency.ISO4217Currencies["CHF"], // Swiss franc + "CZK": currency.ISO4217Currencies["CZK"], // Czech koruna + "DKK": currency.ISO4217Currencies["DKK"], // Danish krone + "EUR": currency.ISO4217Currencies["EUR"], // Euro + "GBP": currency.ISO4217Currencies["GBP"], // Pound sterling + "IDR": currency.ISO4217Currencies["IDR"], // Indonesian rupiah + "JPY": currency.ISO4217Currencies["JPY"], // Japanese yen + "MYR": currency.ISO4217Currencies["MYR"], // Malaysian ringgit + "NOK": currency.ISO4217Currencies["NOK"], // Norwegian krone + "NZD": currency.ISO4217Currencies["NZD"], // New Zealand dollar + "PLN": currency.ISO4217Currencies["PLN"], // Polish złoty + "RON": currency.ISO4217Currencies["RON"], // Romanian leu + "SEK": currency.ISO4217Currencies["SEK"], // Swedish krona/kronor + "SGD": currency.ISO4217Currencies["SGD"], // Singapore dollar + "TRY": currency.ISO4217Currencies["TRY"], // Turkish lira + "USD": currency.ISO4217Currencies["USD"], // United States dollar + + // Unsupported currencies + // "HUF": currency.ISO4217Currencies["HUF"], // Hungarian forint + } +) diff --git a/components/payments/cmd/connectors/internal/connectors/wise/task_fetch_profiles.go b/components/payments/cmd/connectors/internal/connectors/wise/task_fetch_profiles.go index 8b686fb5d2..a765b369de 100644 --- a/components/payments/cmd/connectors/internal/connectors/wise/task_fetch_profiles.go +++ b/components/payments/cmd/connectors/internal/connectors/wise/task_fetch_profiles.go @@ -5,10 +5,12 @@ import ( "encoding/json" "errors" "fmt" + "math" "math/big" "strconv" "time" + "github.com/formancehq/payments/cmd/connectors/internal/connectors/currency" "github.com/formancehq/payments/cmd/connectors/internal/connectors/wise/client" "github.com/formancehq/payments/cmd/connectors/internal/ingestion" "github.com/formancehq/payments/cmd/connectors/internal/metrics" @@ -55,6 +57,7 @@ func taskFetchProfiles(wiseClient *client.Client) task.Task { if err := ingestAccountsBatch( ctx, + logger, connectorID, metricsRegistry, ingester, @@ -103,6 +106,7 @@ func taskFetchProfiles(wiseClient *client.Client) task.Task { func ingestAccountsBatch( ctx context.Context, + logger logging.Logger, connectorID models.ConnectorID, metricsRegistry metrics.MetricsRegistry, ingester ingestion.Ingester, @@ -121,6 +125,13 @@ func ingestAccountsBatch( return err } + precision, ok := supportedCurrenciesWithDecimal[balance.Amount.Currency] + if !ok { + logger.Errorf("currency %s is not supported", balance.Amount.Currency) + metricsRegistry.ConnectorCurrencyNotSupported().Add(ctx, 1, metric.WithAttributes(connectorAttrs...)) + continue + } + accountsBatch = append(accountsBatch, &models.Account{ ID: models.AccountID{ Reference: fmt.Sprintf("%d", balance.ID), @@ -129,7 +140,7 @@ func ingestAccountsBatch( CreatedAt: balance.CreationTime, Reference: fmt.Sprintf("%d", balance.ID), ConnectorID: connectorID, - DefaultAsset: models.Asset(fmt.Sprintf("%s/2", balance.Amount.Currency)), + DefaultAsset: currency.FormatAsset(supportedCurrenciesWithDecimal, balance.Amount.Currency), AccountName: balance.Name, Type: models.AccountTypeInternal, Metadata: map[string]string{ @@ -139,13 +150,13 @@ func ingestAccountsBatch( }) var amount big.Float - _, ok := amount.SetString(balance.Amount.Value.String()) + _, ok = amount.SetString(balance.Amount.Value.String()) if !ok { return fmt.Errorf("failed to parse amount %s", balance.Amount.Value.String()) } 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() balancesBatch = append(balancesBatch, &models.Balance{ @@ -153,7 +164,7 @@ func ingestAccountsBatch( Reference: fmt.Sprintf("%d", balance.ID), ConnectorID: connectorID, }, - Asset: models.Asset(fmt.Sprintf("%s/2", balance.Amount.Currency)), + Asset: currency.FormatAsset(supportedCurrenciesWithDecimal, balance.Amount.Currency), Balance: &amountInt, CreatedAt: now, LastUpdatedAt: now, diff --git a/components/payments/cmd/connectors/internal/connectors/wise/task_fetch_recipient_accounts.go b/components/payments/cmd/connectors/internal/connectors/wise/task_fetch_recipient_accounts.go index 29f2da0600..d02c3f9ffc 100644 --- a/components/payments/cmd/connectors/internal/connectors/wise/task_fetch_recipient_accounts.go +++ b/components/payments/cmd/connectors/internal/connectors/wise/task_fetch_recipient_accounts.go @@ -6,6 +6,7 @@ import ( "fmt" "time" + "github.com/formancehq/payments/cmd/connectors/internal/connectors/currency" "github.com/formancehq/payments/cmd/connectors/internal/connectors/wise/client" "github.com/formancehq/payments/cmd/connectors/internal/ingestion" "github.com/formancehq/payments/cmd/connectors/internal/metrics" @@ -67,7 +68,7 @@ func ingestRecipientAccountsBatch( CreatedAt: time.Now(), Reference: fmt.Sprintf("%d", account.ID), ConnectorID: connectorID, - DefaultAsset: models.Asset(fmt.Sprintf("%s/2", account.Currency)), + DefaultAsset: currency.FormatAsset(supportedCurrenciesWithDecimal, account.Currency), AccountName: account.HolderName, Type: models.AccountTypeExternal, RawData: raw, diff --git a/components/payments/cmd/connectors/internal/connectors/wise/task_fetch_transfers.go b/components/payments/cmd/connectors/internal/connectors/wise/task_fetch_transfers.go index ec20dbcf3a..c2e2caafa8 100644 --- a/components/payments/cmd/connectors/internal/connectors/wise/task_fetch_transfers.go +++ b/components/payments/cmd/connectors/internal/connectors/wise/task_fetch_transfers.go @@ -4,9 +4,11 @@ import ( "context" "encoding/json" "fmt" + "math" "math/big" "time" + "github.com/formancehq/payments/cmd/connectors/internal/connectors/currency" "github.com/formancehq/payments/cmd/connectors/internal/connectors/wise/client" "github.com/formancehq/payments/cmd/connectors/internal/ingestion" "github.com/formancehq/payments/cmd/connectors/internal/metrics" @@ -64,14 +66,21 @@ func taskFetchTransfers(wiseClient *client.Client, profileID uint64) task.Task { return fmt.Errorf("failed to marshal transfer: %w", err) } + precision, ok := supportedCurrenciesWithDecimal[transfer.TargetCurrency] + if !ok { + logger.Errorf("currency %s is not supported", transfer.TargetCurrency) + metricsRegistry.ConnectorCurrencyNotSupported().Add(ctx, 1, metric.WithAttributes(connectorAttrs...)) + continue + } + var amount big.Float - _, ok := amount.SetString(transfer.TargetValue.String()) + _, ok = amount.SetString(transfer.TargetValue.String()) if !ok { return fmt.Errorf("failed to parse amount %s", transfer.TargetValue.String()) } 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{ @@ -89,7 +98,7 @@ func taskFetchTransfers(wiseClient *client.Client, profileID uint64) task.Task { Status: matchTransferStatus(transfer.Status), Scheme: models.PaymentSchemeOther, Amount: &amountInt, - Asset: models.Asset(fmt.Sprintf("%s/2", transfer.TargetCurrency)), + Asset: currency.FormatAsset(supportedCurrenciesWithDecimal, transfer.TargetCurrency), RawData: rawData, }, } diff --git a/components/payments/cmd/connectors/internal/connectors/wise/task_payments.go b/components/payments/cmd/connectors/internal/connectors/wise/task_payments.go index 295ad85cef..c6bd41419e 100644 --- a/components/payments/cmd/connectors/internal/connectors/wise/task_payments.go +++ b/components/payments/cmd/connectors/internal/connectors/wise/task_payments.go @@ -3,6 +3,7 @@ package wise import ( "context" "fmt" + "math" "math/big" "strconv" "time" @@ -96,13 +97,14 @@ func taskInitiatePayment( } 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)))) quote, err := wiseClient.CreateQuote(profileID, curr, amount) if err != nil { diff --git a/components/payments/cmd/connectors/internal/integration/connector.go b/components/payments/cmd/connectors/internal/integration/connector.go index 90f0a7e6e7..4fefa03dc1 100644 --- a/components/payments/cmd/connectors/internal/integration/connector.go +++ b/components/payments/cmd/connectors/internal/integration/connector.go @@ -17,4 +17,6 @@ type Connector interface { Resolve(descriptor models.TaskDescriptor) task.Task // InitiateTransfer is used to initiate a transfer from the connector to a bank account. InitiatePayment(ctx task.ConnectorContext, transfer *models.TransferInitiation) error + // GetSupportedCurrenciesAndDecimals returns a map of supported currencies and their decimals. + SupportedCurrenciesAndDecimals() map[string]int } diff --git a/components/payments/cmd/connectors/internal/integration/connector_test.go b/components/payments/cmd/connectors/internal/integration/connector_test.go index 6abafbb295..642f4e71d9 100644 --- a/components/payments/cmd/connectors/internal/integration/connector_test.go +++ b/components/payments/cmd/connectors/internal/integration/connector_test.go @@ -58,6 +58,10 @@ type BuiltConnector struct { initiatePayment func(ctx task.ConnectorContext, transfer *models.TransferInitiation) error } +func (b *BuiltConnector) SupportedCurrenciesAndDecimals() map[string]int { + return map[string]int{} +} + func (b *BuiltConnector) InitiatePayment(ctx task.ConnectorContext, transfer *models.TransferInitiation) error { if b.initiatePayment != nil { return b.initiatePayment(ctx, transfer) diff --git a/components/payments/cmd/connectors/internal/integration/manager.go b/components/payments/cmd/connectors/internal/integration/manager.go index ba429a76ad..bea0f4975f 100644 --- a/components/payments/cmd/connectors/internal/integration/manager.go +++ b/components/payments/cmd/connectors/internal/integration/manager.go @@ -24,6 +24,7 @@ var ( ErrNotEnabled = errors.New("not enabled") ErrAlreadyRunning = errors.New("already running") ErrConnectorNotFound = errors.New("connector not found") + ErrValidation = errors.New("validation error") ) type connectorManager struct { @@ -359,6 +360,10 @@ func (l *ConnectorsManager[ConnectorConfig]) InitiatePayment(ctx context.Context return ErrConnectorNotFound } + if err := l.validateAssets(ctx, connectorManager, transfer.ConnectorID, transfer.Asset); err != nil { + return err + } + err = connectorManager.connector.InitiatePayment(task.NewConnectorContext(ctx, connectorManager.scheduler), transfer) if err != nil { return fmt.Errorf("initiating transfer: %w", err) @@ -367,6 +372,30 @@ func (l *ConnectorsManager[ConnectorConfig]) InitiatePayment(ctx context.Context return nil } +func (l *ConnectorsManager[ConnectorConfig]) validateAssets( + ctx context.Context, + connectorManager *connectorManager, + connectorID models.ConnectorID, + asset models.Asset, +) error { + supportedCurrencies := connectorManager.connector.SupportedCurrenciesAndDecimals() + currency, precision, err := models.GetCurrencyAndPrecisionFromAsset(asset) + if err != nil { + return errors.Wrap(ErrValidation, err.Error()) + } + + supportedPrecision, ok := supportedCurrencies[currency] + if !ok { + return errors.Wrap(ErrValidation, fmt.Sprintf("currency %s not supported", currency)) + } + + if precision != int64(supportedPrecision) { + return errors.Wrap(ErrValidation, fmt.Sprintf("currency %s has precision %d, but %d is required", currency, precision, supportedPrecision)) + } + + return nil +} + func (l *ConnectorsManager[ConnectorConfig]) Close(ctx context.Context) error { for _, connectorManager := range l.connectors { err := connectorManager.scheduler.Shutdown(ctx) diff --git a/components/payments/cmd/connectors/internal/metrics/metrics.go b/components/payments/cmd/connectors/internal/metrics/metrics.go index 8ba14483f4..acd919c935 100644 --- a/components/payments/cmd/connectors/internal/metrics/metrics.go +++ b/components/payments/cmd/connectors/internal/metrics/metrics.go @@ -10,20 +10,31 @@ const ( ) type MetricsRegistry interface { + ConnectorCurrencyNotSupported() metric.Int64Counter ConnectorObjects() metric.Int64Counter ConnectorObjectsLatency() metric.Int64Histogram ConnectorObjectsErrors() metric.Int64Counter } type metricsRegistry struct { - connectorObjects metric.Int64Counter - connectorObjectsLatency metric.Int64Histogram - connectorObjectsErrors metric.Int64Counter + connectorCurrencyNotSupported metric.Int64Counter + connectorObjects metric.Int64Counter + connectorObjectsLatency metric.Int64Histogram + connectorObjectsErrors metric.Int64Counter } func RegisterMetricsRegistry(meterProvider metric.MeterProvider) (MetricsRegistry, error) { meter := meterProvider.Meter("payments") + connectorCurrencyNotSupported, err := meter.Int64Counter( + "payments_connectors_currency_not_supported", + metric.WithUnit("1"), + metric.WithDescription("Currency not supported by connector"), + ) + if err != nil { + return nil, err + } + connectorObjects, err := meter.Int64Counter( "payments_connectors_objects", metric.WithUnit("1"), @@ -52,12 +63,17 @@ func RegisterMetricsRegistry(meterProvider metric.MeterProvider) (MetricsRegistr } return &metricsRegistry{ - connectorObjects: connectorObjects, - connectorObjectsLatency: connectorObjectLatencies, - connectorObjectsErrors: connectorObjectErrors, + connectorCurrencyNotSupported: connectorCurrencyNotSupported, + connectorObjects: connectorObjects, + connectorObjectsLatency: connectorObjectLatencies, + connectorObjectsErrors: connectorObjectErrors, }, nil } +func (m *metricsRegistry) ConnectorCurrencyNotSupported() metric.Int64Counter { + return m.connectorCurrencyNotSupported +} + func (m *metricsRegistry) ConnectorObjects() metric.Int64Counter { return m.connectorObjects } @@ -76,6 +92,11 @@ func NewNoOpMetricsRegistry() *NoopMetricsRegistry { return &NoopMetricsRegistry{} } +func (m *NoopMetricsRegistry) ConnectorCurrencyNotSupported() metric.Int64Counter { + counter, _ := noop.NewMeterProvider().Meter("payments").Int64Counter("payments_connectors_currency_not_supported") + return counter +} + func (m *NoopMetricsRegistry) ConnectorObjects() metric.Int64Counter { counter, _ := noop.NewMeterProvider().Meter("payments").Int64Counter("payments_connectors_objects") return counter diff --git a/components/payments/cmd/connectors/internal/task/scheduler.go b/components/payments/cmd/connectors/internal/task/scheduler.go index c5fe7266be..a0462feb45 100644 --- a/components/payments/cmd/connectors/internal/task/scheduler.go +++ b/components/payments/cmd/connectors/internal/task/scheduler.go @@ -21,6 +21,7 @@ import ( ) var ( + ErrValidation = errors.New("validation error") ErrAlreadyScheduled = errors.New("already scheduled") ErrUnableToResolve = errors.New("unable to resolve task") ) diff --git a/components/payments/internal/models/payment.go b/components/payments/internal/models/payment.go index 7b5e98cc7a..85de4eda7d 100644 --- a/components/payments/internal/models/payment.go +++ b/components/payments/internal/models/payment.go @@ -7,6 +7,8 @@ import ( "errors" "fmt" "math/big" + "strconv" + "strings" "time" "github.com/gibson042/canonicaljson-go" @@ -171,3 +173,17 @@ func (t PaymentScheme) String() string { func (t Asset) String() string { return string(t) } + +func GetCurrencyAndPrecisionFromAsset(asset Asset) (string, int64, error) { + parts := strings.Split(asset.String(), "/") + if len(parts) != 2 { + return "", 0, errors.New("invalid asset") + } + + precision, err := strconv.ParseInt(parts[1], 10, 64) + if err != nil { + return "", 0, errors.New("invalid asset precision") + } + + return parts[0], precision, nil +}