Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(payments): handle precision from string #1275

Merged
merged 6 commits into from
Feb 27, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import (
"encoding/json"
"errors"
"fmt"
"math"
"math/big"
"time"

Expand Down Expand Up @@ -141,19 +140,6 @@ func ingestAccountsBatch(
})

balance := account.Balance
// No need to check if the currency is supported for accounts and
// balances.
precision := supportedCurrenciesWithDecimal[*balance.Amount.Currency]

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

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

balanceTimestamp, err := ParseAtlarTimestamp(balance.Timestamp)
if err != nil {
return err
Expand All @@ -164,7 +150,7 @@ func ingestAccountsBatch(
ConnectorID: connectorID,
},
Asset: currency.FormatAsset(supportedCurrenciesWithDecimal, *balance.Amount.Currency),
Balance: &amountInt,
Balance: big.NewInt(*balance.Amount.Value),
CreatedAt: balanceTimestamp,
LastUpdatedAt: time.Now().UTC(),
ConnectorID: connectorID,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,8 @@ type Payment struct {
} `json:"viban"`
InstructedDate interface{} `json:"instructedDate"`
DebitAmount struct {
Currency string `json:"currency"`
Amount float64 `json:"amount"`
Currency string `json:"currency"`
Amount json.Number `json:"amount"`
} `json:"debitAmount"`
DebitValueDate time.Time `json:"debitValueDate"`
FxRate interface{} `json:"fxRate"`
Expand All @@ -48,8 +48,8 @@ type Payment struct {
DebtorName interface{} `json:"debtorName"`
DebtorAddress interface{} `json:"debtorAddress"`
Amount struct {
Currency string `json:"currency"`
Amount float64 `json:"amount"`
Currency string `json:"currency"`
Amount json.Number `json:"amount"`
} `json:"amount"`
ValueDate interface{} `json:"valueDate"`
ChargeBearer interface{} `json:"chargeBearer"`
Expand All @@ -72,8 +72,8 @@ type Payment struct {
Country string `json:"country"`
} `json:"viban"`
CreditAmount struct {
Currency string `json:"currency"`
Amount float64 `json:"amount"`
Currency string `json:"currency"`
Amount json.Number `json:"amount"`
} `json:"creditAmount"`
CreditValueDate time.Time `json:"creditValueDate"`
FxRate interface{} `json:"fxRate"`
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import (
"context"
"encoding/json"
"fmt"
"math/big"
"net/http"
"time"

Expand All @@ -25,8 +24,8 @@ type PaymentRequest struct {
DebtorReference string `json:"debtorReference"`
CurrencyOfTransfer string `json:"currencyOfTransfer"`
Amount struct {
Currency string `json:"currency"`
Amount *big.Float `json:"amount"`
Currency string `json:"currency"`
Amount json.Number `json:"amount"`
} `json:"amount"`
ChargeBearer string `json:"chargeBearer"`
CreditorAccount *PaymentAccount `json:"creditorAccount"`
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,6 @@ import (
"encoding/json"
"errors"
"fmt"
"math"
"math/big"
"time"

"github.com/formancehq/payments/cmd/connectors/internal/connectors"
Expand Down Expand Up @@ -128,23 +126,19 @@ func ingestAccountsBatch(
// balances.
precision := supportedCurrenciesWithDecimal[balance.Currency]

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

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

now := time.Now()
balanceBatch = append(balanceBatch, &models.Balance{
AccountID: models.AccountID{
Reference: account.AccountID,
ConnectorID: connectorID,
},
Asset: currency.FormatAsset(supportedCurrenciesWithDecimal, balance.Currency),
Balance: &amountInt,
Balance: amount,
CreatedAt: now,
LastUpdatedAt: now,
ConnectorID: connectorID,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,6 @@ package bankingcircle
import (
"context"
"encoding/json"
"math"
"math/big"

"github.com/formancehq/payments/cmd/connectors/internal/connectors"
"github.com/formancehq/payments/cmd/connectors/internal/connectors/bankingcircle/client"
Expand Down Expand Up @@ -87,10 +85,10 @@ func ingestBatch(
continue
}

var amount big.Float
amount.SetFloat64(paymentEl.Transfer.Amount.Amount)
var amountInt big.Int
amount.Mul(&amount, big.NewFloat(math.Pow(10, float64(precision)))).Int(&amountInt)
amount, err := currency.GetAmountWithPrecisionFromString(paymentEl.Transfer.Amount.Amount.String(), precision)
if err != nil {
return err
}

batchElement := ingestion.PaymentBatchElement{
Payment: &models.Payment{
Expand All @@ -106,8 +104,8 @@ func ingestBatch(
ConnectorID: connectorID,
Status: matchPaymentStatus(paymentEl.Status),
Scheme: models.PaymentSchemeOther,
Amount: &amountInt,
InitialAmount: &amountInt,
Amount: amount,
InitialAmount: amount,
Asset: currency.FormatAsset(supportedCurrenciesWithDecimal, paymentEl.Transfer.Amount.Currency),
RawData: raw,
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,8 @@ package bankingcircle

import (
"context"
"encoding/json"
"errors"
"math"
"math/big"
"time"

"github.com/formancehq/payments/cmd/connectors/internal/connectors"
Expand Down Expand Up @@ -96,8 +95,10 @@ func initiatePayment(
return err
}

amount := big.NewFloat(0).SetInt(transfer.Amount)
amount = amount.Quo(amount, big.NewFloat(math.Pow(10, float64(precision))))
amount, err := currency.GetStringAmountFromBigIntWithPrecision(transfer.Amount, precision)
if err != nil {
return err
}

var sourceAccount *client.Account
sourceAccount, err = bankingCircleClient.GetAccount(ctx, transfer.SourceAccountID.Reference)
Expand Down Expand Up @@ -138,11 +139,11 @@ func initiatePayment(
DebtorReference: transfer.Description,
CurrencyOfTransfer: curr,
Amount: struct {
Currency string "json:\"currency\""
Amount *big.Float "json:\"amount\""
Currency string "json:\"currency\""
Amount json.Number "json:\"amount\""
}{
Currency: curr,
Amount: amount,
Amount: json.Number(amount),
},
ChargeBearer: "SHA",
CreditorAccount: &client.PaymentAccount{
Expand Down Expand Up @@ -171,11 +172,11 @@ func initiatePayment(
DebtorReference: transfer.Description,
CurrencyOfTransfer: curr,
Amount: struct {
Currency string "json:\"currency\""
Amount *big.Float "json:\"amount\""
Currency string "json:\"currency\""
Amount json.Number "json:\"amount\""
}{
Currency: curr,
Amount: amount,
Amount: json.Number(amount),
},
ChargeBearer: "SHA",
CreditorAccount: &client.PaymentAccount{
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
package currency

import (
"bytes"
"fmt"
"math/big"
"strings"

"github.com/pkg/errors"
)

// This package provides a way to convert a string amount to a big.Int
// with a given precision. The precision is the number of decimals that
// the amount has. For example, if the amount is 123.45 and the precision is 2,
// the amount will be 12345.
// We also provide a way to convert a big.Int to a string amount with a given
// precision.
// We developed this package because we need to convert amounts from strings,
// and apply the precision to them to have minor units. If we do that with
// the big package using floats and divisions/multiplcations, we will
// sometimes loose precision, which is unacceptable.
// Here is an example of loosing precision with the big package:

var (
// ErrInvalidAmount is returned when the amount is invalid
ErrInvalidAmount = fmt.Errorf("invalid amount")
// ErrInvalidPrecision is returned when the precision is inferior to the
// number of decimals in the amount or negative
ErrInvalidPrecision = fmt.Errorf("invalid precision")
)

func GetAmountWithPrecisionFromString(amountString string, precision int) (*big.Int, error) {
if precision < 0 {
return nil, errors.Wrap(ErrInvalidPrecision, fmt.Sprintf("precision is negative: %d", precision))
}

parts := strings.Split(amountString, ".")

lengthParts := len(parts)

if lengthParts > 2 || lengthParts == 0 {
// More than one dot, invalid amount
return nil, errors.Wrap(ErrInvalidAmount, fmt.Sprintf("got multiple dots in amount: %s", amountString))
}

if lengthParts == 1 {
// No dot, which means it's an integer
for p := 0; p < precision; p++ {
amountString += "0"
}
res, ok := new(big.Int).SetString(amountString, 10)
if !ok {
return nil, errors.Wrap(ErrInvalidAmount, fmt.Sprintf("invalid amount: %s", amountString))
}
return res, nil
}

// Here we are in the case where we have one dot, which means we have a
// decimal amount
decimalPart := parts[1]
lengthDecimalPart := len(decimalPart)
switch {
case lengthDecimalPart == precision:
// The decimal part has the same length as the precision, we can
// concatenate the two parts and return the result
res, ok := new(big.Int).SetString(parts[0]+decimalPart, 10)
if !ok {
return nil, errors.Wrap(ErrInvalidAmount, fmt.Sprintf("invalid amount computed: %s from amount %s", parts[0]+decimalPart, amountString))
}
return res, nil

case lengthDecimalPart < precision:
// The decimal part is shorter than the precision, we need to add
// some zeros at the end of the decimal part
for p := 0; p < precision-lengthDecimalPart; p++ {
decimalPart += "0"
}
res, ok := new(big.Int).SetString(parts[0]+decimalPart, 10)
if !ok {
return nil, errors.Wrap(ErrInvalidAmount, fmt.Sprintf("invalid amount computed: %s from amount %s", parts[0]+decimalPart, amountString))
}
return res, nil

default:
// The decimal part is longer than the precision, we have to send an
// error because we don't want to loose the precision
return nil, ErrInvalidPrecision
}
}

func GetStringAmountFromBigIntWithPrecision(amount *big.Int, precision int) (string, error) {
if precision < 0 {
return "", errors.Wrap(ErrInvalidPrecision, fmt.Sprintf("precision is negative: %d", precision))
}

amountString := amount.String()
amountStringLength := len(amountString)

if precision == 0 {
// Nothing to do
return amountString, nil
}

decimalPart := bytes.NewBufferString("")
for p := precision; p > 0; p-- {
paul-nicolas marked this conversation as resolved.
Show resolved Hide resolved
if amountStringLength < p {
decimalPart.WriteByte('0')
continue
}
decimalPart.WriteByte(amountString[amountStringLength-p])
}

if amountStringLength < precision || amountStringLength == precision {
return "0." + decimalPart.String(), nil
}

// Here we are in the case where the amount has more digits than the
// precision, we need to add a dot at the right place
return amountString[:amountStringLength-precision] + "." + decimalPart.String(), nil
}
Loading
Loading