diff --git a/common/transfer_statements.go b/common/transfer_statements.go new file mode 100644 index 0000000..4dd409c --- /dev/null +++ b/common/transfer_statements.go @@ -0,0 +1,119 @@ +package common + +import ( + "context" + "fmt" + "strings" + "time" + + "git.grassecon.net/urdt/ussd/internal/storage" + dataserviceapi "github.com/grassrootseconomics/ussd-data-service/pkg/api" +) + +// TransferMetadata helps organize data fields +type TransferMetadata struct { + Senders string + Recipients string + TransferValues string + Addresses string + TxHashes string + Dates string + Symbols string + Decimals string +} + +// ProcessTransfers converts transfers into formatted strings +func ProcessTransfers(transfers []dataserviceapi.Last10TxResponse) TransferMetadata { + var data TransferMetadata + var senders, recipients, transferValues, addresses, txHashes, dates, symbols, decimals []string + + for _, t := range transfers { + senders = append(senders, t.Sender) + recipients = append(recipients, t.Recipient) + + // Scale down the amount + scaledBalance := ScaleDownBalance(t.TransferValue, t.TokenDecimals) + transferValues = append(transferValues, scaledBalance) + + addresses = append(addresses, t.ContractAddress) + txHashes = append(txHashes, t.TxHash) + dates = append(dates, fmt.Sprintf("%s", t.DateBlock)) + symbols = append(symbols, t.TokenSymbol) + decimals = append(decimals, t.TokenDecimals) + } + + data.Senders = strings.Join(senders, "\n") + data.Recipients = strings.Join(recipients, "\n") + data.TransferValues = strings.Join(transferValues, "\n") + data.Addresses = strings.Join(addresses, "\n") + data.TxHashes = strings.Join(txHashes, "\n") + data.Dates = strings.Join(dates, "\n") + data.Symbols = strings.Join(symbols, "\n") + data.Decimals = strings.Join(decimals, "\n") + + return data +} + +// GetTransferData retrieves and matches transfer data +// returns a formatted string of the full transaction/statement +func GetTransferData(ctx context.Context, db storage.PrefixDb, publicKey string, index int) (string, error) { + keys := []string{"txfrom", "txto", "txval", "txaddr", "txhash", "txdate", "txsym"} + data := make(map[string]string) + + for _, key := range keys { + value, err := db.Get(ctx, []byte(key)) + if err != nil { + return "", fmt.Errorf("failed to get %s: %v", key, err) + } + data[key] = string(value) + } + + // Split the data + senders := strings.Split(string(data["txfrom"]), "\n") + recipients := strings.Split(string(data["txto"]), "\n") + values := strings.Split(string(data["txval"]), "\n") + addresses := strings.Split(string(data["txaddr"]), "\n") + hashes := strings.Split(string(data["txhash"]), "\n") + dates := strings.Split(string(data["txdate"]), "\n") + syms := strings.Split(string(data["txsym"]), "\n") + + // Check if index is within range + if index < 1 || index > len(senders) { + return "", fmt.Errorf("transaction not found: index %d out of range", index) + } + + // Adjust for 0-based indexing + i := index - 1 + transactionType := "received" + party := fmt.Sprintf("from: %s", strings.TrimSpace(senders[i])) + if strings.TrimSpace(senders[i]) == publicKey { + transactionType = "sent" + party = fmt.Sprintf("to: %s", strings.TrimSpace(recipients[i])) + } + + formattedDate := formatDate(strings.TrimSpace(dates[i])) + + // Build the full transaction detail + detail := fmt.Sprintf( + "%s %s %s\n%s\ncontract address: %s\ntxhash: %s\ndate: %s", + transactionType, + strings.TrimSpace(values[i]), + strings.TrimSpace(syms[i]), + party, + strings.TrimSpace(addresses[i]), + strings.TrimSpace(hashes[i]), + formattedDate, + ) + + return detail, nil +} + +// Helper function to format date in desired output +func formatDate(dateStr string) string { + parsedDate, err := time.Parse("2006-01-02 15:04:05 -0700 MST", dateStr) + if err != nil { + fmt.Println("Error parsing date:", err) + return "" + } + return parsedDate.Format("2006-01-02 03:04:05 PM") +} diff --git a/internal/handlers/handlerservice.go b/internal/handlers/handlerservice.go index fe88c1f..c77e82c 100644 --- a/internal/handlers/handlerservice.go +++ b/internal/handlers/handlerservice.go @@ -118,6 +118,9 @@ func (ls *LocalHandlerService) GetHandler(accountService remote.AccountServiceIn ls.DbRs.AddLocalFunc("reset_others_pin", ussdHandlers.ResetOthersPin) ls.DbRs.AddLocalFunc("save_others_temporary_pin", ussdHandlers.SaveOthersTemporaryPin) ls.DbRs.AddLocalFunc("get_current_profile_info", ussdHandlers.GetCurrentProfileInfo) + ls.DbRs.AddLocalFunc("check_transactions", ussdHandlers.CheckTransactions) + ls.DbRs.AddLocalFunc("get_transactions", ussdHandlers.GetTransactionsList) + ls.DbRs.AddLocalFunc("view_statement", ussdHandlers.ViewTransactionStatement) return ussdHandlers, nil } diff --git a/internal/handlers/ussd/menuhandler.go b/internal/handlers/ussd/menuhandler.go index 6311444..f4edb5d 100644 --- a/internal/handlers/ussd/menuhandler.go +++ b/internal/handlers/ussd/menuhandler.go @@ -1658,3 +1658,172 @@ func (h *Handlers) GetVoucherDetails(ctx context.Context, sym string, input []by return res, nil } + +// CheckTransactions retrieves the transactions from the API using the "PublicKey" and stores to prefixDb +func (h *Handlers) CheckTransactions(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") + } + + flag_no_transfers, _ := h.flagManager.GetFlag("flag_no_transfers") + flag_api_error, _ := h.flagManager.GetFlag("flag_api_error") + + store := h.userdataStore + publicKey, err := store.ReadEntry(ctx, sessionId, common.DATA_PUBLIC_KEY) + if err != nil { + logg.ErrorCtxf(ctx, "failed to read publicKey entry with", "key", common.DATA_PUBLIC_KEY, "error", err) + return res, err + } + + // Fetch transactions from the API using the public key + transactionsResp, err := h.accountService.FetchTransactions(ctx, string(publicKey)) + if err != nil { + res.FlagSet = append(res.FlagSet, flag_api_error) + logg.ErrorCtxf(ctx, "failed on FetchTransactions", "error", err) + return res, err + } + + // Return if there are no transactions + if len(transactionsResp) == 0 { + res.FlagSet = append(res.FlagSet, flag_no_transfers) + return res, nil + } + + data := common.ProcessTransfers(transactionsResp) + + // Store all transaction data + dataMap := map[string]string{ + "txfrom": data.Senders, + "txto": data.Recipients, + "txval": data.TransferValues, + "txaddr": data.Addresses, + "txhash": data.TxHashes, + "txdate": data.Dates, + "txsym": data.Symbols, + "txdeci": data.Decimals, + } + + for key, value := range dataMap { + if err := h.prefixDb.Put(ctx, []byte(key), []byte(value)); err != nil { + logg.ErrorCtxf(ctx, "failed to write to prefixDb", "error", err) + return res, err + } + } + + res.FlagReset = append(res.FlagReset, flag_no_transfers) + + return res, nil +} + +// GetTransactionsList fetches the list of transactions and formats them +func (h *Handlers) GetTransactionsList(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") + } + store := h.userdataStore + publicKey, err := store.ReadEntry(ctx, sessionId, common.DATA_PUBLIC_KEY) + if err != nil { + logg.ErrorCtxf(ctx, "failed to read publicKey entry with", "key", common.DATA_PUBLIC_KEY, "error", err) + return res, err + } + + // Read transactions from the store and format them + TransactionSenders, err := h.prefixDb.Get(ctx, []byte("txfrom")) + if err != nil { + logg.ErrorCtxf(ctx, "Failed to read the TransactionSenders from prefixDb", "error", err) + return res, err + } + TransactionSyms, err := h.prefixDb.Get(ctx, []byte("txsym")) + if err != nil { + logg.ErrorCtxf(ctx, "Failed to read the TransactionSyms from prefixDb", "error", err) + return res, err + } + TransactionValues, err := h.prefixDb.Get(ctx, []byte("txval")) + if err != nil { + logg.ErrorCtxf(ctx, "Failed to read the TransactionValues from prefixDb", "error", err) + return res, err + } + TransactionDates, err := h.prefixDb.Get(ctx, []byte("txdate")) + if err != nil { + logg.ErrorCtxf(ctx, "Failed to read the TransactionDates from prefixDb", "error", err) + return res, err + } + + // Parse the data + senders := strings.Split(string(TransactionSenders), "\n") + syms := strings.Split(string(TransactionSyms), "\n") + values := strings.Split(string(TransactionValues), "\n") + dates := strings.Split(string(TransactionDates), "\n") + + var formattedTransactions []string + for i := 0; i < len(senders); i++ { + sender := strings.TrimSpace(senders[i]) + sym := strings.TrimSpace(syms[i]) + value := strings.TrimSpace(values[i]) + date := strings.Split(strings.TrimSpace(dates[i]), " ")[0] + + status := "received" + if sender == string(publicKey) { + status = "sent" + } + + formattedTransactions = append(formattedTransactions, fmt.Sprintf("%d:%s %s %s %s", i+1, status, value, sym, date)) + } + + res.Content = strings.Join(formattedTransactions, "\n") + + return res, nil +} + +// ViewTransactionStatement retrieves the transaction statement +// and displays it to the user +func (h *Handlers) ViewTransactionStatement(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") + } + store := h.userdataStore + publicKey, err := store.ReadEntry(ctx, sessionId, common.DATA_PUBLIC_KEY) + if err != nil { + logg.ErrorCtxf(ctx, "failed to read publicKey entry with", "key", common.DATA_PUBLIC_KEY, "error", err) + return res, err + } + + flag_incorrect_statement, _ := h.flagManager.GetFlag("flag_incorrect_statement") + + inputStr := string(input) + if inputStr == "0" || inputStr == "99" || inputStr == "11" || inputStr == "22" { + res.FlagReset = append(res.FlagReset, flag_incorrect_statement) + return res, nil + } + + // Convert input string to integer + index, err := strconv.Atoi(strings.TrimSpace(inputStr)) + if err != nil { + return res, fmt.Errorf("invalid input: must be a number between 1 and 10") + } + + if index < 1 || index > 10 { + return res, fmt.Errorf("invalid input: index must be between 1 and 10") + } + + statement, err := common.GetTransferData(ctx, h.prefixDb, string(publicKey), index) + if err != nil { + return res, fmt.Errorf("failed to retrieve transfer data: %v", err) + } + + if statement == "" { + res.FlagSet = append(res.FlagSet, flag_incorrect_statement) + return res, nil + } + + res.FlagReset = append(res.FlagReset, flag_incorrect_statement) + res.Content = statement + + return res, nil +} diff --git a/internal/testutil/testtag/onlinetest.go b/internal/testutil/testtag/onlinetest.go index 92cbb14..835ba0d 100644 --- a/internal/testutil/testtag/onlinetest.go +++ b/internal/testutil/testtag/onlinetest.go @@ -2,8 +2,8 @@ package testtag -import "git.grassecon.net/urdt/ussd/internal/handlers/server" +import "git.grassecon.net/urdt/ussd/remote" var ( - AccountService server.AccountServiceInterface + AccountService remote.AccountServiceInterface ) diff --git a/services/registration/check_statement b/services/registration/check_statement new file mode 100644 index 0000000..0e989db --- /dev/null +++ b/services/registration/check_statement @@ -0,0 +1 @@ +Please enter your PIN to view statement: \ No newline at end of file diff --git a/services/registration/check_statement.vis b/services/registration/check_statement.vis new file mode 100644 index 0000000..d79b5ca --- /dev/null +++ b/services/registration/check_statement.vis @@ -0,0 +1,12 @@ +LOAD check_transactions 0 +RELOAD check_transactions +CATCH no_transfers flag_no_transfers 1 +LOAD authorize_account 6 +MOUT back 0 +MOUT quit 9 +HALT +RELOAD authorize_account +CATCH incorrect_pin flag_incorrect_pin 1 +INCMP _ 0 +INCMP quit 9 +INCMP transactions * diff --git a/services/registration/check_statement_swa b/services/registration/check_statement_swa new file mode 100644 index 0000000..468364f --- /dev/null +++ b/services/registration/check_statement_swa @@ -0,0 +1 @@ +Tafadhali weka PIN yako kuona taarifa ya matumizi: \ No newline at end of file diff --git a/services/registration/my_account.vis b/services/registration/my_account.vis index 43ee6a2..2b6289e 100644 --- a/services/registration/my_account.vis +++ b/services/registration/my_account.vis @@ -11,5 +11,6 @@ INCMP main 0 INCMP edit_profile 1 INCMP change_language 2 INCMP balances 3 +INCMP check_statement 4 INCMP pin_management 5 INCMP address 6 diff --git a/services/registration/no_transfers b/services/registration/no_transfers new file mode 100644 index 0000000..3439806 --- /dev/null +++ b/services/registration/no_transfers @@ -0,0 +1 @@ +No transfers history \ No newline at end of file diff --git a/services/registration/no_transfers.vis b/services/registration/no_transfers.vis new file mode 100644 index 0000000..832ef22 --- /dev/null +++ b/services/registration/no_transfers.vis @@ -0,0 +1,5 @@ +MOUT back 0 +MOUT quit 9 +HALT +INCMP ^ 0 +INCMP quit 9 diff --git a/services/registration/no_transfers_swa b/services/registration/no_transfers_swa new file mode 100644 index 0000000..1f82e82 --- /dev/null +++ b/services/registration/no_transfers_swa @@ -0,0 +1 @@ +Hakuna historia kwa akaunti yako \ No newline at end of file diff --git a/services/registration/pp.csv b/services/registration/pp.csv index 406cc22..cd6a633 100644 --- a/services/registration/pp.csv +++ b/services/registration/pp.csv @@ -19,3 +19,5 @@ flag,flag_api_call_error,25,this is set when communication to an external servic flag,flag_no_active_voucher,26,this is set when a user does not have an active voucher flag,flag_admin_privilege,27,this is set when a user has admin privileges. flag,flag_unregistered_number,28,this is set when an unregistered phonenumber tries to perform an action +flag,flag_no_transfers,29,this is set when a user does not have any transactions +flag,flag_incorrect_statement,30,this is set when the selected statement is invalid diff --git a/services/registration/transactions b/services/registration/transactions new file mode 100644 index 0000000..8152c42 --- /dev/null +++ b/services/registration/transactions @@ -0,0 +1 @@ +{{.get_transactions}} \ No newline at end of file diff --git a/services/registration/transactions.vis b/services/registration/transactions.vis new file mode 100644 index 0000000..b21f1dc --- /dev/null +++ b/services/registration/transactions.vis @@ -0,0 +1,15 @@ +LOAD get_transactions 0 +MAP get_transactions +MOUT back 0 +MOUT quit 99 +MNEXT next 11 +MPREV prev 22 +HALT +LOAD view_statement 0 +RELOAD view_statement +CATCH . flag_incorrect_statement 1 +INCMP ^ 0 +INCMP quit 99 +INCMP > 11 +INCMP < 22 +INCMP view_statement * diff --git a/services/registration/transactions_swa b/services/registration/transactions_swa new file mode 100644 index 0000000..8152c42 --- /dev/null +++ b/services/registration/transactions_swa @@ -0,0 +1 @@ +{{.get_transactions}} \ No newline at end of file diff --git a/services/registration/view_statement b/services/registration/view_statement new file mode 100644 index 0000000..1285cf9 --- /dev/null +++ b/services/registration/view_statement @@ -0,0 +1 @@ +{{.view_statement}} \ No newline at end of file diff --git a/services/registration/view_statement.vis b/services/registration/view_statement.vis new file mode 100644 index 0000000..4dde523 --- /dev/null +++ b/services/registration/view_statement.vis @@ -0,0 +1,10 @@ +MAP view_statement +MOUT back 0 +MOUT quit 9 +MNEXT next 11 +MPREV prev 22 +HALT +INCMP _ 0 +INCMP quit 9 +INCMP > 11 +INCMP < 22