Skip to content
This repository was archived by the owner on Jul 1, 2024. It is now read-only.

Commit bae2c89

Browse files
authored
Merge pull request #45 from oxygenpay/develop
feat: system transfers (#44)
2 parents 66cbc35 + 4ffa753 commit bae2c89

File tree

20 files changed

+1052
-33
lines changed

20 files changed

+1052
-33
lines changed

cmd/list_balances.go

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
package cmd
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"os"
7+
"strings"
8+
9+
"github.com/olekukonko/tablewriter"
10+
"github.com/oxygenpay/oxygen/internal/app"
11+
"github.com/oxygenpay/oxygen/internal/money"
12+
"github.com/oxygenpay/oxygen/internal/service/wallet"
13+
"github.com/spf13/cobra"
14+
)
15+
16+
var listBalancesCommand = &cobra.Command{
17+
Use: "list-balances",
18+
Short: "List all balances including system balances",
19+
Run: listBalances,
20+
}
21+
22+
func listBalances(_ *cobra.Command, _ []string) {
23+
var (
24+
ctx = context.Background()
25+
cfg = resolveConfig()
26+
service = app.New(ctx, cfg)
27+
walletsService = service.Locator().WalletService()
28+
blockchainService = service.Locator().BlockchainService()
29+
logger = service.Logger()
30+
)
31+
32+
opts := wallet.ListAllBalancesOpts{
33+
WithUSD: true,
34+
WithSystemBalances: true,
35+
HideEmpty: true,
36+
}
37+
38+
balances, err := walletsService.ListAllBalances(ctx, opts)
39+
if err != nil {
40+
logger.Error().Err(err).Msg("Unable to list wallets")
41+
}
42+
43+
t := tablewriter.NewWriter(os.Stdout)
44+
defer t.Render()
45+
46+
t.SetBorder(false)
47+
t.SetAutoWrapText(false)
48+
t.SetHeaderAlignment(tablewriter.ALIGN_LEFT)
49+
t.SetAlignment(tablewriter.ALIGN_LEFT)
50+
51+
t.SetHeader([]string{"id", "type", "entity Id", "currency", "test", "amount", "usd"})
52+
53+
add := func(b *wallet.Balance) {
54+
currency, err := blockchainService.GetCurrencyByTicker(b.Currency)
55+
if err != nil {
56+
logger.Error().Err(err)
57+
return
58+
}
59+
60+
t.Append(balanceAsRow(currency, b))
61+
}
62+
63+
for _, b := range balances[wallet.EntityTypeMerchant] {
64+
add(b)
65+
}
66+
for _, b := range balances[wallet.EntityTypeWallet] {
67+
add(b)
68+
}
69+
for _, b := range balances[wallet.EntityTypeSystem] {
70+
add(b)
71+
}
72+
}
73+
74+
func balanceAsRow(currency money.CryptoCurrency, b *wallet.Balance) []string {
75+
isTest := b.NetworkID != currency.NetworkID
76+
77+
line := fmt.Sprintf(
78+
"%d,%s,%d,%s,%t,%s,%s",
79+
b.ID,
80+
b.EntityType,
81+
b.EntityID,
82+
b.Currency,
83+
isTest,
84+
b.Amount.String(),
85+
b.UsdAmount.String(),
86+
)
87+
88+
return strings.Split(line, ",")
89+
}

cmd/root.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,10 @@ func init() {
7777
createUserCommand.PersistentFlags().BoolVar(&overridePassword, "override-password", false, "overrides password if user already exists")
7878

7979
rootCmd.AddCommand(listWalletsCommand)
80+
rootCmd.AddCommand(listBalancesCommand)
81+
82+
topupBalanceSetup(topupBalanceCommand)
83+
rootCmd.AddCommand(topupBalanceCommand)
8084

8185
rand.Seed(time.Now().Unix())
8286
}

cmd/topup_balance.go

Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
package cmd
2+
3+
import (
4+
"bufio"
5+
"context"
6+
"fmt"
7+
"os"
8+
"strings"
9+
10+
"github.com/oxygenpay/oxygen/internal/app"
11+
"github.com/oxygenpay/oxygen/internal/money"
12+
"github.com/oxygenpay/oxygen/internal/service/processing"
13+
"github.com/oxygenpay/oxygen/internal/service/wallet"
14+
"github.com/oxygenpay/oxygen/internal/util"
15+
"github.com/samber/lo"
16+
"github.com/spf13/cobra"
17+
)
18+
19+
var topupBalanceCommand = &cobra.Command{
20+
Use: "topup-balance",
21+
Short: "Topup Merchant's balance using 'system' funds",
22+
Run: topupBalance,
23+
}
24+
25+
var topupBalanceArgs = struct {
26+
MerchantID *int64
27+
Ticker *string
28+
Amount *string
29+
Comment *string
30+
IsTest *bool
31+
}{
32+
MerchantID: util.Ptr(int64(0)),
33+
Ticker: util.Ptr(""),
34+
Amount: util.Ptr(""),
35+
Comment: util.Ptr(""),
36+
IsTest: util.Ptr(false),
37+
}
38+
39+
func topupBalance(_ *cobra.Command, _ []string) {
40+
var (
41+
ctx = context.Background()
42+
cfg = resolveConfig()
43+
service = app.New(ctx, cfg)
44+
blockchainService = service.Locator().BlockchainService()
45+
merchantService = service.Locator().MerchantService()
46+
walletService = service.Locator().WalletService()
47+
processingService = service.Locator().ProcessingService()
48+
logger = service.Logger()
49+
exit = func(err error, message string) { logger.Fatal().Err(err).Msg(message) }
50+
)
51+
52+
// 1. Get input
53+
currency, err := blockchainService.GetCurrencyByTicker(*topupBalanceArgs.Ticker)
54+
if err != nil {
55+
exit(err, "invalid ticker")
56+
}
57+
58+
amount, err := money.CryptoFromStringFloat(currency.Ticker, *topupBalanceArgs.Amount, currency.Decimals)
59+
if err != nil {
60+
exit(err, "invalid amount")
61+
}
62+
63+
merchant, err := merchantService.GetByID(ctx, *topupBalanceArgs.MerchantID, false)
64+
if err != nil {
65+
exit(err, "invalid merchant id")
66+
}
67+
68+
if *topupBalanceArgs.Comment == "" {
69+
exit(nil, "comment should not be empty")
70+
}
71+
72+
isTest := *topupBalanceArgs.IsTest
73+
comment := *topupBalanceArgs.Comment
74+
75+
// 2. Locate system balance
76+
balances, err := walletService.ListAllBalances(ctx, wallet.ListAllBalancesOpts{WithSystemBalances: true})
77+
if err != nil {
78+
exit(err, "unable to list balances")
79+
}
80+
81+
systemBalance, found := lo.Find(balances[wallet.EntityTypeSystem], func(b *wallet.Balance) bool {
82+
tickerMatches := b.Currency == currency.Ticker
83+
networkMatches := b.NetworkID == currency.ChooseNetwork(isTest)
84+
85+
return tickerMatches && networkMatches
86+
})
87+
88+
if !found {
89+
exit(err, "unable to locate system balance")
90+
}
91+
92+
logger.Info().
93+
Str("amount", amount.String()).
94+
Str("currency", currency.Ticker).
95+
Str("merchant.name", merchant.Name).
96+
Int64("merchant.id", merchant.ID).
97+
Str("merchant.uuid", merchant.UUID.String()).
98+
Str("system_balance", systemBalance.Amount.String()).
99+
Msg("Performing internal topup from the system balance")
100+
101+
// 3. Confirm
102+
if !confirm("Are you sure you want to continue?") {
103+
logger.Info().Msg("Aborting.")
104+
return
105+
}
106+
107+
// 4. Perform topup
108+
logger.Info().Msg("Sending...")
109+
110+
input := processing.TopupInput{
111+
Currency: currency,
112+
Amount: amount,
113+
Comment: comment,
114+
IsTest: isTest,
115+
}
116+
117+
out, err := processingService.TopupMerchantFromSystem(ctx, merchant.ID, input)
118+
if err != nil {
119+
exit(err, "unable to topup the balance")
120+
}
121+
122+
logger.
123+
Info().
124+
Int64("payment.id", out.Payment.ID).
125+
Int64("tx.id", out.Transaction.ID).
126+
Str("tx.usd_amount", out.Transaction.USDAmount.String()).
127+
Str("merchant.balance", out.MerchantBalance.Amount.String()).
128+
Msg("Done")
129+
}
130+
131+
func topupBalanceSetup(cmd *cobra.Command) {
132+
f := cmd.Flags()
133+
134+
f.Int64Var(topupBalanceArgs.MerchantID, "merchant-id", 0, "Merchant ID")
135+
f.StringVar(topupBalanceArgs.Ticker, "ticker", "", "Ticker")
136+
f.StringVar(topupBalanceArgs.Amount, "amount", "0", "Amount")
137+
f.StringVar(topupBalanceArgs.Comment, "comment", "", "Comment")
138+
f.BoolVar(topupBalanceArgs.IsTest, "is-test", false, "Test balance")
139+
140+
for _, name := range []string{"merchant-id", "ticker", "amount", "comment"} {
141+
if err := cmd.MarkFlagRequired(name); err != nil {
142+
panic(name + ": " + err.Error())
143+
}
144+
}
145+
}
146+
147+
func confirm(message string) bool {
148+
reader := bufio.NewReader(os.Stdin)
149+
fmt.Printf("%s (y/n): ", message)
150+
151+
response, err := reader.ReadString('\n')
152+
if err != nil {
153+
return false
154+
}
155+
156+
response = strings.ToLower(strings.TrimSpace(response))
157+
158+
return response == "y" || response == "yes"
159+
}

internal/db/repository/balances.sql.go

Lines changed: 45 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

internal/db/repository/helpers.go

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,5 @@ func MoneyToNumeric(m money.Money) pgtype.Numeric {
112112

113113
func MoneyToNegNumeric(m money.Money) pgtype.Numeric {
114114
bigInt, _ := m.BigInt()
115-
116115
return BigIntToNumeric(big.NewInt(0).Neg(bigInt))
117116
}

internal/db/repository/querier.go

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

internal/money/money.go

Lines changed: 29 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,12 @@ func (m Money) Decimals() int64 {
144144

145145
func (m Money) String() string {
146146
stringRaw := m.StringRaw()
147+
148+
isNegative := m.IsNegative()
149+
if isNegative {
150+
stringRaw = stringRaw[1:]
151+
}
152+
147153
l, d := len(stringRaw), int(m.decimals)
148154

149155
var result string
@@ -159,10 +165,16 @@ func (m Money) String() string {
159165
}
160166

161167
if m.moneyType == Fiat {
162-
return strings.TrimSuffix(result, ".00")
168+
result = strings.TrimSuffix(result, ".00")
169+
} else {
170+
result = strings.TrimRight(strings.TrimRight(result, "0"), ".")
163171
}
164172

165-
return strings.TrimRight(strings.TrimRight(result, "0"), ".")
173+
if isNegative {
174+
result = "-" + result
175+
}
176+
177+
return result
166178
}
167179

168180
func (m Money) StringRaw() string {
@@ -202,8 +214,22 @@ func (m Money) Add(amount Money) (Money, error) {
202214
return NewFromBigInt(m.moneyType, m.ticker, a.Add(a, b), m.decimals)
203215
}
204216

205-
// Sub subtracts money of the same type.
217+
// Sub subtracts money of the same type. Restricts having negative values.
206218
func (m Money) Sub(amount Money) (Money, error) {
219+
out, err := m.SubNegative(amount)
220+
if err != nil {
221+
return Money{}, err
222+
}
223+
224+
if out.IsNegative() {
225+
return Money{}, ErrNegative
226+
}
227+
228+
return out, nil
229+
}
230+
231+
// SubNegative subtracts money allowing negative outcome
232+
func (m Money) SubNegative(amount Money) (Money, error) {
207233
if !m.CompatibleTo(amount) {
208234
return Money{}, errors.Wrapf(
209235
ErrIncompatibleMoney,
@@ -220,10 +246,6 @@ func (m Money) Sub(amount Money) (Money, error) {
220246
return Money{}, nil
221247
}
222248

223-
if m.IsNegative() {
224-
return Money{}, ErrNegative
225-
}
226-
227249
return m, nil
228250
}
229251

0 commit comments

Comments
 (0)