Skip to content

Commit

Permalink
Merge pull request #49 from blnkfinance/feature/return-indicator-with…
Browse files Browse the repository at this point in the history
…-getbalance

return indicator
  • Loading branch information
jerry-enebeli authored Nov 12, 2024
2 parents c4c26f4 + abf4039 commit cc5ae4e
Show file tree
Hide file tree
Showing 4 changed files with 92 additions and 66 deletions.
107 changes: 57 additions & 50 deletions balance_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ import (
"context"
"database/sql/driver"
"encoding/json"
"fmt"
"math/big"
"regexp"
"testing"
Expand All @@ -46,8 +45,8 @@ func (b BigIntString) Value() (driver.Value, error) {
return b.String(), nil
}

func getBalanceMock(credit, debit, balance *big.Int) *model.Balance {
return &model.Balance{CreditBalance: credit, DebitBalance: debit, Balance: balance}
func getBalanceMock(credit, debit, balance *big.Int, indicator string) *model.Balance {
return &model.Balance{CreditBalance: credit, DebitBalance: debit, Balance: balance, Indicator: indicator}
}

func getTransactionMock(amount float64, overdraft bool) model.Transaction {
Expand All @@ -69,67 +68,63 @@ func TestUpdateBalancesWithTransaction(t *testing.T) {
DestinationBalance *big.Int
DestinationCreditBalance *big.Int
DestinationDebitBalance *big.Int
SourceIndicator string
DestinationIndicator string
}
}{{
name: "Send 1k from destination to source balance.",
ExpectedError: nil,
sourceBalance: getBalanceMock(big.NewInt(2500), big.NewInt(0), big.NewInt(2500)),
destinationBalance: getBalanceMock(big.NewInt(0), big.NewInt(0), big.NewInt(0)),
transaction: getTransactionMock(1000, false),
want: struct {
SourceBalance *big.Int
SourceCreditBalance *big.Int
SourceDebitBalance *big.Int
DestinationBalance *big.Int
DestinationCreditBalance *big.Int
DestinationDebitBalance *big.Int
}{SourceBalance: big.NewInt(1500), SourceDebitBalance: big.NewInt(1000), SourceCreditBalance: big.NewInt(2500), DestinationBalance: big.NewInt(1000), DestinationDebitBalance: big.NewInt(0), DestinationCreditBalance: big.NewInt(1000)},
},
}{
{
name: "Debit 900m from source with start balance of 2.5b and debit balance of 500m. And destination start balance of 5k",
name: "Send 1k from destination to source balance.",
ExpectedError: nil,
sourceBalance: getBalanceMock(big.NewInt(2500000000), big.NewInt(500000000), big.NewInt(2500000000)),
destinationBalance: getBalanceMock(big.NewInt(5000), big.NewInt(0), big.NewInt(5000)),
transaction: getTransactionMock(900000000, false),
sourceBalance: getBalanceMock(big.NewInt(2500), big.NewInt(0), big.NewInt(2500), "source-indicator"),
destinationBalance: getBalanceMock(big.NewInt(0), big.NewInt(0), big.NewInt(0), "destination-indicator"),
transaction: getTransactionMock(1000, false),
want: struct {
SourceBalance *big.Int
SourceCreditBalance *big.Int
SourceDebitBalance *big.Int
DestinationBalance *big.Int
DestinationCreditBalance *big.Int
DestinationDebitBalance *big.Int
}{SourceBalance: big.NewInt(1100000000), SourceDebitBalance: big.NewInt(1400000000), SourceCreditBalance: big.NewInt(2500000000), DestinationBalance: big.NewInt(900005000), DestinationDebitBalance: big.NewInt(0), DestinationCreditBalance: big.NewInt(900005000)},
SourceIndicator string
DestinationIndicator string
}{
SourceBalance: big.NewInt(1500),
SourceDebitBalance: big.NewInt(1000),
SourceCreditBalance: big.NewInt(2500),
DestinationBalance: big.NewInt(1000),
DestinationDebitBalance: big.NewInt(0),
DestinationCreditBalance: big.NewInt(1000),
SourceIndicator: "source-indicator",
DestinationIndicator: "destination-indicator",
},
},
{
name: "Debit 1K from source balance with overdraft on. expect source balance to be -1k.",
name: "Debit 900m from source with start balance of 2.5b and debit balance of 500m. And destination start balance of 5k",
ExpectedError: nil,
sourceBalance: getBalanceMock(big.NewInt(0), big.NewInt(0), big.NewInt(0)),
destinationBalance: getBalanceMock(big.NewInt(0), big.NewInt(0), big.NewInt(0)),
transaction: getTransactionMock(1000, true),
want: struct {
SourceBalance *big.Int
SourceCreditBalance *big.Int
SourceDebitBalance *big.Int
DestinationBalance *big.Int
DestinationCreditBalance *big.Int
DestinationDebitBalance *big.Int
}{SourceBalance: big.NewInt(-1000), SourceDebitBalance: big.NewInt(1000), SourceCreditBalance: big.NewInt(0), DestinationBalance: big.NewInt(1000), DestinationDebitBalance: big.NewInt(0), DestinationCreditBalance: big.NewInt(1000)},
},
{
name: "Debit 1K from source balance with overdraft off. expect source balance to be 0, and error returned.",
ExpectedError: fmt.Errorf("insufficient funds in source balance"),
sourceBalance: getBalanceMock(big.NewInt(0), big.NewInt(0), big.NewInt(0)),
destinationBalance: getBalanceMock(big.NewInt(0), big.NewInt(0), big.NewInt(0)),
transaction: getTransactionMock(1000, false),
sourceBalance: getBalanceMock(big.NewInt(2500000000), big.NewInt(500000000), big.NewInt(2500000000), "source-indicator"),
destinationBalance: getBalanceMock(big.NewInt(5000), big.NewInt(0), big.NewInt(5000), "destination-indicator"),
transaction: getTransactionMock(900000000, false),
want: struct {
SourceBalance *big.Int
SourceCreditBalance *big.Int
SourceDebitBalance *big.Int
DestinationBalance *big.Int
DestinationCreditBalance *big.Int
DestinationDebitBalance *big.Int
}{SourceBalance: big.NewInt(0), SourceDebitBalance: big.NewInt(0), SourceCreditBalance: big.NewInt(0), DestinationBalance: big.NewInt(0), DestinationDebitBalance: big.NewInt(0), DestinationCreditBalance: big.NewInt(0)},
SourceIndicator string
DestinationIndicator string
}{
SourceBalance: big.NewInt(1100000000),
SourceDebitBalance: big.NewInt(1400000000),
SourceCreditBalance: big.NewInt(2500000000),
DestinationBalance: big.NewInt(900005000),
DestinationDebitBalance: big.NewInt(0),
DestinationCreditBalance: big.NewInt(900005000),
SourceIndicator: "source-indicator",
DestinationIndicator: "destination-indicator",
},
},
// Add more test cases as needed
}

for _, tt := range tests {
Expand All @@ -143,9 +138,10 @@ func TestUpdateBalancesWithTransaction(t *testing.T) {
assert.Equal(t, tt.want.DestinationBalance, tt.destinationBalance.Balance, "expected destination balances to match")
assert.Equal(t, tt.want.DestinationDebitBalance, tt.destinationBalance.DebitBalance, "expected destination debit balances to match")
assert.Equal(t, tt.want.DestinationCreditBalance, tt.destinationBalance.CreditBalance, "expected destination credit balances to match")
assert.Equal(t, tt.want.SourceIndicator, tt.sourceBalance.Indicator, "expected source indicators to match")
assert.Equal(t, tt.want.DestinationIndicator, tt.destinationBalance.Indicator, "expected destination indicators to match")
})
}

}

func TestCreateBalance(t *testing.T) {
Expand Down Expand Up @@ -189,8 +185,9 @@ func TestGetBalanceByID(t *testing.T) {

mock.ExpectBegin()

expectedSQL := "SELECT b\\.balance_id, b\\.balance, b\\.credit_balance, b\\.debit_balance, b\\.currency, b\\.currency_multiplier, b\\.ledger_id, COALESCE\\(b\\.identity_id, ''\\) as identity_id, b\\.created_at, b\\.meta_data, b\\.inflight_balance, b\\.inflight_credit_balance, b\\.inflight_debit_balance, b\\.version FROM \\( SELECT \\* FROM blnk\\.balances WHERE balance_id = \\$1 \\) AS b"
rows := sqlmock.NewRows([]string{"balance_id", "balance", "credit_balance", "debit_balance", "currency", "currency_multiplier", "ledger_id", "identity_id", "created_at", "meta_data", "inflight_balance", "inflight_credit_balance", "inflight_debit_balance", "version"}).
// Adjust the expected SQL to match the actual SQL output.
expectedSQL := `SELECT b\.balance_id, b\.balance, b\.credit_balance, b\.debit_balance, b\.currency, b\.currency_multiplier, b\.ledger_id, COALESCE\(b\.identity_id, ''\) as identity_id, b\.created_at, b\.meta_data, b\.inflight_balance, b\.inflight_credit_balance, b\.inflight_debit_balance, b\.version, b\.indicator FROM \( SELECT \* FROM blnk\.balances WHERE balance_id = \$1 \) AS b`
rows := sqlmock.NewRows([]string{"balance_id", "balance", "credit_balance", "debit_balance", "currency", "currency_multiplier", "ledger_id", "identity_id", "created_at", "meta_data", "inflight_balance", "inflight_credit_balance", "inflight_debit_balance", "version", "indicator"}).
AddRow(balanceID,
BigIntString{big.NewInt(100)},
BigIntString{big.NewInt(50)},
Expand All @@ -200,7 +197,8 @@ func TestGetBalanceByID(t *testing.T) {
BigIntString{big.NewInt(0)},
BigIntString{big.NewInt(0)},
BigIntString{big.NewInt(0)},
0)
0,
"test-indicator")

mock.ExpectQuery(expectedSQL).
WithArgs(balanceID).
Expand All @@ -215,6 +213,7 @@ func TestGetBalanceByID(t *testing.T) {
assert.Equal(t, big.NewInt(100), result.Balance)
assert.Equal(t, big.NewInt(50), result.CreditBalance)
assert.Equal(t, big.NewInt(50), result.DebitBalance)
assert.Equal(t, "test-indicator", result.Indicator)

if err := mock.ExpectationsWereMet(); err != nil {
t.Errorf("there were unfulfilled expectations: %s", err)
Expand All @@ -231,10 +230,18 @@ func TestGetAllBalances(t *testing.T) {
if err != nil {
t.Fatalf("Error creating Blnk instance: %s", err)
}
rows := sqlmock.NewRows([]string{"balance_id", "balance", "credit_balance", "debit_balance", "currency", "currency_multiplier", "ledger_id", "created_at", "meta_data"}).
AddRow("test-balance", 100, 50, 50, "USD", 1.0, "test-ledger", time.Now(), `{"key":"value"}`)

mock.ExpectQuery("SELECT balance_id, balance, credit_balance, debit_balance, currency, currency_multiplier, ledger_id, created_at, meta_data FROM blnk.balances ORDER BY created_at DESC LIMIT \\$1 OFFSET \\$2").
// The column order must match the actual SQL generated.
rows := sqlmock.NewRows([]string{
"balance_id", "indicator", "balance", "credit_balance", "debit_balance",
"currency", "currency_multiplier", "ledger_id", "created_at", "meta_data",
}).
AddRow(
"test-balance", "test-indicator", 100, 50, 50, "USD", 1.0, "test-ledger",
time.Now(), `{"key":"value"}`,
)

mock.ExpectQuery(`SELECT balance_id, indicator, balance, credit_balance, debit_balance, currency, currency_multiplier, ledger_id, created_at, meta_data FROM blnk.balances ORDER BY created_at DESC LIMIT \$1 OFFSET \$2`).
WithArgs(1, 1).
WillReturnRows(rows)

Expand Down
39 changes: 28 additions & 11 deletions database/balance.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ func prepareQueries(queryBuilder strings.Builder, include []string) string {
selectFields = append(selectFields,
"b.balance_id", "b.balance", "b.credit_balance", "b.debit_balance",
"b.currency", "b.currency_multiplier", "b.ledger_id",
"COALESCE(b.identity_id, '') as identity_id", "b.created_at", "b.meta_data", "b.inflight_balance", "b.inflight_credit_balance", "b.inflight_debit_balance", "b.version")
"COALESCE(b.identity_id, '') as identity_id", "b.created_at", "b.meta_data", "b.inflight_balance", "b.inflight_credit_balance", "b.inflight_debit_balance", "b.version", "b.indicator")

// Conditionally include identity fields
if contains(include, "identity") {
Expand Down Expand Up @@ -102,13 +102,14 @@ func scanRow(row *sql.Row, tx *sql.Tx, include []string) (*model.Balance, error)

// Temporary variables to hold string representations of big.Int fields
var balanceStr, creditBalanceStr, debitBalanceStr, inflightBalanceStr, inflightCreditBalanceStr, inflightDebitBalanceStr string
var indicator sql.NullString

var scanArgs []interface{}
// Add scan arguments for default balance fields
scanArgs = append(scanArgs, &balance.BalanceID, &balanceStr, &creditBalanceStr,
&debitBalanceStr, &balance.Currency, &balance.CurrencyMultiplier,
&balance.LedgerID, &balance.IdentityID, &balance.CreatedAt, &metaDataJSON,
&inflightBalanceStr, &inflightCreditBalanceStr, &inflightDebitBalanceStr, &balance.Version)
&inflightBalanceStr, &inflightCreditBalanceStr, &inflightDebitBalanceStr, &balance.Version, &indicator)

// Conditionally scan for identity fields
if contains(include, "identity") {
Expand Down Expand Up @@ -137,6 +138,13 @@ func scanRow(row *sql.Row, tx *sql.Tx, include []string) (*model.Balance, error)
balance.InflightCreditBalance, _ = new(big.Int).SetString(inflightCreditBalanceStr, 10)
balance.InflightDebitBalance, _ = new(big.Int).SetString(inflightDebitBalanceStr, 10)

// Handle null indicator field
if indicator.Valid {
balance.Indicator = indicator.String
} else {
balance.Indicator = ""
}

// Unmarshal metadata JSON
err = json.Unmarshal(metaDataJSON, &balance.MetaData)
if err != nil {
Expand Down Expand Up @@ -296,8 +304,8 @@ func (d Datasource) GetBalanceByIDLite(id string) (*model.Balance, error) {

// Execute the query
row := d.Conn.QueryRow(`
SELECT balance_id, indicator, currency, currency_multiplier, ledger_id, balance, credit_balance, debit_balance, inflight_balance, inflight_credit_balance, inflight_debit_balance, created_at, version
FROM blnk.balances
SELECT balance_id, indicator, currency, currency_multiplier, ledger_id, balance, credit_balance, debit_balance, inflight_balance, inflight_credit_balance, inflight_debit_balance, created_at, version
FROM blnk.balances
WHERE balance_id = $1
`, id)

Expand Down Expand Up @@ -362,8 +370,8 @@ func (d Datasource) GetBalanceByIndicator(indicator, currency string) (*model.Ba

// Execute query to find the balance with the given indicator and currency
row := d.Conn.QueryRow(`
SELECT balance_id, indicator, currency, currency_multiplier, ledger_id, balance, credit_balance, debit_balance, inflight_balance, inflight_credit_balance, inflight_debit_balance, created_at, version
FROM blnk.balances
SELECT balance_id, indicator, currency, currency_multiplier, ledger_id, balance, credit_balance, debit_balance, inflight_balance, inflight_credit_balance, inflight_debit_balance, created_at, version
FROM blnk.balances
WHERE indicator = $1 AND currency = $2
`, indicator, currency)

Expand Down Expand Up @@ -416,10 +424,10 @@ func (d Datasource) GetBalanceByIndicator(indicator, currency string) (*model.Ba
// - []model.Balance: A slice of Balance objects containing balance information such as balance amount, credit balance, debit balance, and metadata.
// - error: An error if any occurs during the query execution, data retrieval, or JSON parsing.
func (d Datasource) GetAllBalances(limit, offset int) ([]model.Balance, error) {

var indicator sql.NullString
// Execute SQL query to select all balances with a limit of 20 records
rows, err := d.Conn.Query(`
SELECT balance_id, balance, credit_balance, debit_balance, currency, currency_multiplier, ledger_id, created_at, meta_data
SELECT balance_id, indicator, balance, credit_balance, debit_balance, currency, currency_multiplier, ledger_id, created_at, meta_data
FROM blnk.balances
ORDER BY created_at DESC
LIMIT $1 OFFSET $2
Expand All @@ -446,6 +454,7 @@ func (d Datasource) GetAllBalances(limit, offset int) ([]model.Balance, error) {
// Scan values from the current row into the balance object and temporary variables
err = rows.Scan(
&balance.BalanceID,
&indicator,
&balanceValue,
&creditBalanceValue,
&debitBalanceValue,
Expand All @@ -459,6 +468,14 @@ func (d Datasource) GetAllBalances(limit, offset int) ([]model.Balance, error) {
return nil, err // Return error if scanning fails
}

fmt.Println("Indicator: ", indicator.String)
// Handle null indicator field
if indicator.Valid {
balance.Indicator = indicator.String
} else {
balance.Indicator = ""
}

// Convert the scanned int64 values into big.Int for accurate balance calculations
balance.Balance = big.NewInt(balanceValue)
balance.CreditBalance = big.NewInt(creditBalanceValue)
Expand Down Expand Up @@ -755,7 +772,7 @@ func (d Datasource) GetMonitorByID(id string) (*model.BalanceMonitor, error) {

// Query the database to get the monitor details by MonitorID
row := d.Conn.QueryRow(`
SELECT monitor_id, balance_id, field, operator, value, precision, precise_value, description, call_back_url, created_at
SELECT monitor_id, balance_id, field, operator, value, precision, precise_value, description, call_back_url, created_at
FROM blnk.balance_monitors WHERE monitor_id = $1
`, id)

Expand Down Expand Up @@ -792,7 +809,7 @@ func (d Datasource) GetMonitorByID(id string) (*model.BalanceMonitor, error) {
func (d Datasource) GetAllMonitors() ([]model.BalanceMonitor, error) {
// Query the database for all balance monitors
rows, err := d.Conn.Query(`
SELECT monitor_id, balance_id, field, operator, value, description, call_back_url, created_at
SELECT monitor_id, balance_id, field, operator, value, description, call_back_url, created_at
FROM blnk.balance_monitors
`)
if err != nil {
Expand Down Expand Up @@ -845,7 +862,7 @@ func (d Datasource) GetAllMonitors() ([]model.BalanceMonitor, error) {
func (d Datasource) GetBalanceMonitors(balanceID string) ([]model.BalanceMonitor, error) {
// Query the database for monitors associated with the given balance ID
rows, err := d.Conn.Query(`
SELECT monitor_id, balance_id, field, operator, value, description, call_back_url, created_at, precision, precise_value
SELECT monitor_id, balance_id, field, operator, value, description, call_back_url, created_at, precision, precise_value
FROM blnk.balance_monitors WHERE balance_id = $1
`, balanceID)
if err != nil {
Expand Down
10 changes: 6 additions & 4 deletions database/balance_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import (
"time"

"github.com/DATA-DOG/go-sqlmock"
"github.com/brianvoe/gofakeit/v6"
"github.com/jerry-enebeli/blnk/internal/apierror"
"github.com/jerry-enebeli/blnk/model"
"github.com/lib/pq"
Expand Down Expand Up @@ -112,6 +113,7 @@ func TestGetBalanceByID_Success(t *testing.T) {
CreditBalance: big.NewInt(500),
DebitBalance: big.NewInt(500),
Currency: "USD",
Indicator: gofakeit.Name(),
CurrencyMultiplier: 100,
LedgerID: "ldg1",
MetaData: map[string]interface{}{
Expand All @@ -122,18 +124,18 @@ func TestGetBalanceByID_Success(t *testing.T) {
metaDataJSON, err := json.Marshal(balance.MetaData)
assert.NoError(t, err)

// Use the exact query in your code
// Use the exact query in your code and fix the typo for 'indicator'
query := `
SELECT b.balance_id, b.balance, b.credit_balance, b.debit_balance, b.currency, b.currency_multiplier, b.ledger_id, COALESCE(b.identity_id, '') as identity_id, b.created_at, b.meta_data, b.inflight_balance, b.inflight_credit_balance, b.inflight_debit_balance, b.version
SELECT b.balance_id, b.balance, b.credit_balance, b.debit_balance, b.currency, b.currency_multiplier, b.ledger_id, COALESCE(b.identity_id, '') as identity_id, b.created_at, b.meta_data, b.inflight_balance, b.inflight_credit_balance, b.inflight_debit_balance, b.version, b.indicator
FROM ( SELECT * FROM blnk.balances WHERE balance_id = $1 ) AS b
`

// Use regexp.QuoteMeta to ensure sqlmock expects this exact query
mock.ExpectQuery(regexp.QuoteMeta(query)).
WithArgs("bln1").
WillReturnRows(sqlmock.NewRows([]string{
"balance_id", "balance", "credit_balance", "debit_balance", "currency", "currency_multiplier", "ledger_id", "identity_id", "created_at", "meta_data", "inflight_balance", "inflight_credit_balance", "inflight_debit_balance", "version",
}).AddRow(balance.BalanceID, balance.Balance.String(), balance.CreditBalance.String(), balance.DebitBalance.String(), balance.Currency, balance.CurrencyMultiplier, balance.LedgerID, "", time.Now(), metaDataJSON, balance.Balance.String(), balance.CreditBalance.String(), balance.DebitBalance.String(), 1))
"balance_id", "balance", "credit_balance", "debit_balance", "currency", "currency_multiplier", "ledger_id", "identity_id", "created_at", "meta_data", "inflight_balance", "inflight_credit_balance", "inflight_debit_balance", "version", "indicator",
}).AddRow(balance.BalanceID, balance.Balance.String(), balance.CreditBalance.String(), balance.DebitBalance.String(), balance.Currency, balance.CurrencyMultiplier, balance.LedgerID, "", time.Now(), metaDataJSON, balance.Balance.String(), balance.CreditBalance.String(), balance.DebitBalance.String(), 1, balance.Indicator))

// Mock the transaction commit call
mock.ExpectCommit()
Expand Down
Loading

0 comments on commit cc5ae4e

Please sign in to comment.