sarafu-vise/handlers/application/balance.go
alfred-mk 43c4b64b42
Some checks failed
release / docker (push) Has been cancelled
correctly calculate the credit
2026-02-23 16:20:36 +03:00

246 lines
7.5 KiB
Go
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package application
import (
"context"
"fmt"
"strconv"
"git.defalsify.org/vise.git/db"
"git.defalsify.org/vise.git/resource"
"git.grassecon.net/grassrootseconomics/sarafu-vise/store"
storedb "git.grassecon.net/grassrootseconomics/sarafu-vise/store/db"
dataserviceapi "github.com/grassrootseconomics/ussd-data-service/pkg/api"
"gopkg.in/leonelquinteros/gotext.v1"
)
// CheckBalance retrieves the balance of the active voucher and sets
// the balance as the result content.
func (h *MenuHandlers) CheckBalance(ctx context.Context, sym string, input []byte) (resource.Result, error) {
var (
res resource.Result
err error
content string
)
sessionId, ok := ctx.Value("SessionId").(string)
if !ok {
return res, fmt.Errorf("missing session")
}
store := h.userdataStore
// get the active sym and active balance
activeSym, err := store.ReadEntry(ctx, sessionId, storedb.DATA_ACTIVE_SYM)
if err != nil {
logg.InfoCtxf(ctx, "could not find the activeSym in checkBalance:", "err", err)
if !db.IsNotFound(err) {
logg.ErrorCtxf(ctx, "failed to read activeSym entry with", "key", storedb.DATA_ACTIVE_SYM, "error", err)
return res, err
}
}
activeBal, err := store.ReadEntry(ctx, sessionId, storedb.DATA_ACTIVE_BAL)
if err != nil {
if !db.IsNotFound(err) {
logg.ErrorCtxf(ctx, "failed to read activeBal entry with", "key", storedb.DATA_ACTIVE_BAL, "error", err)
return res, err
}
}
accAlias, err := store.ReadEntry(ctx, sessionId, storedb.DATA_ACCOUNT_ALIAS)
if err != nil {
if !db.IsNotFound(err) {
logg.ErrorCtxf(ctx, "failed to read account alias entry with", "key", storedb.DATA_ACCOUNT_ALIAS, "error", err)
return res, err
}
}
content, err = loadUserContent(ctx, string(activeSym), string(activeBal), string(accAlias))
if err != nil {
return res, err
}
res.Content = content
return res, nil
}
// loadUserContent loads the main user content in the main menu: the alias, balance and active symbol associated with active voucher
func loadUserContent(ctx context.Context, activeSym, balance, alias string) (string, error) {
var content string
code := codeFromCtx(ctx)
l := gotext.NewLocale(translationDir, code)
l.AddDomain("default")
// Format the balance to 2 decimal places or default to 0.00
formattedAmount, err := store.TruncateDecimalString(balance, 2)
if err != nil {
formattedAmount = "0.00"
}
// format the final outputs
balStr := fmt.Sprintf("%s %s", formattedAmount, activeSym)
if alias != "" {
content = l.Get("%s\nBalance: %s\n", alias, balStr)
} else {
content = l.Get("Balance: %s\n", balStr)
}
return content, nil
}
// FetchCommunityBalance retrieves and displays the balance for community accounts in user's preferred language.
func (h *MenuHandlers) FetchCommunityBalance(ctx context.Context, sym string, input []byte) (resource.Result, error) {
var res resource.Result
// retrieve the language code from the context
code := codeFromCtx(ctx)
// Initialize the localization system with the appropriate translation directory
l := gotext.NewLocale(translationDir, code)
l.AddDomain("default")
//TODO:
//Check if the address is a community account,if then,get the actual balance
res.Content = l.Get("Community Balance: 0.00")
return res, nil
}
// CalculateCreditAndDebt calls the API to get the credit and debt
// uses the pretium rates to convert the value to Ksh
func (h *MenuHandlers) CalculateCreditAndDebt(ctx context.Context, sym string, input []byte) (resource.Result, error) {
var res resource.Result
sessionId, ok := ctx.Value("SessionId").(string)
if !ok {
return res, fmt.Errorf("missing session")
}
code := codeFromCtx(ctx)
l := gotext.NewLocale(translationDir, code)
l.AddDomain("default")
flag_api_call_error, _ := h.flagManager.GetFlag("flag_api_call_error")
// Fetch session data
_, activeBal, activeSym, activeAddress, publicKey, activeDecimal, err := h.getSessionData(ctx, sessionId)
if err != nil {
res.Content = l.Get("Credit: %s KSH\nDebt: %s %s\n", "0", "0", string(activeSym))
return res, nil
}
res.FlagReset = append(res.FlagReset, flag_api_call_error)
// Resolve active pool
activePoolAddress, _, err := h.resolveActivePoolDetails(ctx, sessionId)
if err != nil {
return res, err
}
// Fetch swappable vouchers (pool view)
swappableVouchers, err := h.accountService.GetPoolSwappableFromVouchers(ctx, string(activePoolAddress), string(publicKey))
if err != nil {
logg.ErrorCtxf(ctx, "failed on GetPoolSwappableFromVouchers", "error", err)
res.Content = l.Get("Credit: %s KSH\nDebt: %s %s\n", "0", "0", string(activeSym))
return res, nil
}
if len(swappableVouchers) == 0 {
res.Content = l.Get("Credit: %s KSH\nDebt: %s %s\n", "0", "0", string(activeSym))
return res, nil
}
// Fetch ALL wallet vouchers (voucher holdings view)
allVouchers, err := h.accountService.FetchVouchers(ctx, string(publicKey))
if err != nil {
logg.ErrorCtxf(ctx, "failed on FetchVouchers", "error", err)
return res, nil
}
// CREDIT calculation
// Rule:
// 1. Swap quote of active voucher → first stable in pool from GetPoolSwappableFromVouchers
// 2. PLUS all stable balances from FetchVouchers
scaledCredit := "0"
// 1. Find first stable voucher in POOL (for swap target)
var firstPoolStable *dataserviceapi.TokenHoldings
for i := range swappableVouchers {
if isStableVoucher(swappableVouchers[i].TokenAddress) {
firstPoolStable = &swappableVouchers[i]
break
}
}
// 2. If pool has a stable, get swap quote
if firstPoolStable != nil {
finalAmountStr, err := store.ParseAndScaleAmount(
string(activeBal),
string(activeDecimal),
)
if err != nil {
return res, err
}
// swap active -> FIRST stable from pool list
r, err := h.accountService.GetPoolSwapQuote(ctx, finalAmountStr, string(publicKey), string(activeAddress), string(activePoolAddress), firstPoolStable.TokenAddress)
if err != nil {
res.FlagSet = append(res.FlagSet, flag_api_call_error)
res.Content = l.Get("Your request failed. Please try again later.")
logg.ErrorCtxf(ctx, "failed on poolSwap", "error", err)
return res, nil
}
// scale using REAL stable decimals
finalQuote := store.ScaleDownBalance(r.OutValue, firstPoolStable.TokenDecimals)
scaledCredit = store.AddDecimalStrings(scaledCredit, finalQuote)
}
// 3. Add ALL wallet stable balances (from FetchVouchers)
for _, v := range allVouchers {
if isStableVoucher(v.TokenAddress) {
scaled := store.ScaleDownBalance(v.Balance, v.TokenDecimals)
scaledCredit = store.AddDecimalStrings(scaledCredit, scaled)
}
}
// DEBT calculation
// Rule:
// - Default = 0
// - If active is stable → remain 0
// - If active is non-stable and exists in pool → use pool balance
scaledDebt := "0"
if !isStableVoucher(string(activeAddress)) {
for _, v := range swappableVouchers {
if v.TokenSymbol == string(activeSym) {
scaledDebt = store.ScaleDownBalance(v.Balance, v.TokenDecimals)
break
}
}
}
formattedDebt, _ := store.TruncateDecimalString(scaledDebt, 2)
// Fetch MPESA rates
rates, err := h.accountService.GetMpesaOnrampRates(ctx)
if err != nil {
res.FlagSet = append(res.FlagSet, flag_api_call_error)
res.Content = l.Get("Your request failed. Please try again later.")
logg.ErrorCtxf(ctx, "failed on GetMpesaOnrampRates", "error", err)
return res, nil
}
creditFloat, _ := strconv.ParseFloat(scaledCredit, 64)
creditKsh := fmt.Sprintf("%f", creditFloat*rates.Buy)
kshFormattedCredit, _ := store.TruncateDecimalString(creditKsh, 0)
res.Content = l.Get(
"Credit: %s KSH\nDebt: %s %s\n",
kshFormattedCredit,
formattedDebt,
string(activeSym),
)
return res, nil
}