fixed fee direction

This commit is contained in:
Stephan D
2026-03-05 13:24:41 +01:00
parent 1e376da719
commit 4a5e26b03a
69 changed files with 8677 additions and 82 deletions

View File

@@ -47,16 +47,22 @@ func parseCommand(text string) Command {
}
func supportedCommandsMessage() string {
lines := make([]string, 0, len(supportedCommands)+1)
lines = append(lines, "Supported commands:")
lines := make([]string, 0, len(supportedCommands)+2)
lines = append(lines, "*Supported Commands*")
lines = append(lines, "")
for _, cmd := range supportedCommands {
lines = append(lines, cmd.Slash())
lines = append(lines, markdownCommand(cmd))
}
return strings.Join(lines, "\n")
}
func confirmationCommandsMessage() string {
return "Confirm operation?\n\n" + CommandConfirm.Slash() + "\n" + CommandCancel.Slash()
return strings.Join([]string{
"*Confirm Operation*",
"",
"Use " + markdownCommand(CommandConfirm) + " to execute.",
"Use " + markdownCommand(CommandCancel) + " to abort.",
}, "\n")
}
func helpMessage(accountCode string, currency string) string {
@@ -70,16 +76,18 @@ func helpMessage(accountCode string, currency string) string {
}
lines := []string{
"Treasury bot help",
"*Treasury Bot Help*",
"",
"Attached account: " + accountCode + " (" + currency + ")",
"*Attached account:* " + markdownCode(accountCode) + " (" + markdownCode(currency) + ")",
"",
"How to use:",
"1) Start funding with " + CommandFund.Slash() + " or withdrawal with " + CommandWithdraw.Slash(),
"2) Enter amount as decimal, dot separator, no currency (example: 1250.75)",
"3) Confirm with " + CommandConfirm.Slash() + " or abort with " + CommandCancel.Slash(),
"*How to use*",
"1. Start funding with " + markdownCommand(CommandFund) + " or withdrawal with " + markdownCommand(CommandWithdraw) + ".",
"2. Enter amount as decimal with dot separator and no currency.",
" Example: " + markdownCode("1250.75"),
"3. Confirm with " + markdownCommand(CommandConfirm) + " or abort with " + markdownCommand(CommandCancel) + ".",
"",
"After confirmation there is a cooldown window. You can cancel during it with " + CommandCancel.Slash() + ".",
"*Cooldown*",
"After confirmation there is a cooldown window. You can cancel during it with " + markdownCommand(CommandCancel) + ".",
"You will receive a follow-up message with execution success or failure.",
}
return strings.Join(lines, "\n")

View File

@@ -0,0 +1,18 @@
package bot
import (
"strings"
)
func markdownCode(value string) string {
value = strings.TrimSpace(value)
if value == "" {
value = "N/A"
}
value = strings.ReplaceAll(value, "`", "'")
return "`" + value + "`"
}
func markdownCommand(command Command) string {
return markdownCode(command.Slash())
}

View File

@@ -14,10 +14,10 @@ import (
"go.uber.org/zap"
)
const unauthorizedMessage = "Sorry, your Telegram account is not authorized to perform treasury operations."
const unauthorizedChatMessage = "Sorry, this Telegram chat is not authorized to perform treasury operations."
const unauthorizedMessage = "*Unauthorized*\nYour Telegram account is not allowed to perform treasury operations."
const unauthorizedChatMessage = "*Unauthorized Chat*\nThis Telegram chat is not allowed to perform treasury operations."
const amountInputHint = "Enter amount as a decimal number using a dot separator and without currency.\nExample: 1250.75"
const amountInputHint = "*Amount format*\nEnter amount as a decimal number using a dot separator and without currency.\nExample: `1250.75`"
type SendTextFunc func(ctx context.Context, chatID string, text string) error
@@ -232,7 +232,7 @@ func (r *Router) startAmountDialog(ctx context.Context, userID, accountID, chatI
if r.logger != nil {
r.logger.Warn("Failed to check active treasury request", zap.Error(err), zap.String("telegram_user_id", userID), zap.String("ledger_account_id", accountID))
}
_ = r.sendText(ctx, chatID, "Unable to check pending treasury operations right now. Please try again.")
_ = r.sendText(ctx, chatID, "*Temporary issue*\nUnable to check pending treasury operations right now. Please try again.")
return
}
if active != nil {
@@ -274,22 +274,22 @@ func (r *Router) captureAmount(ctx context.Context, userID, accountID, chatID st
if typed, ok := err.(limitError); ok {
switch typed.LimitKind() {
case "per_operation":
_ = r.sendText(ctx, chatID, "Amount exceeds allowed limit.\n\nMax per operation: "+typed.LimitMax()+"\n\nEnter another amount or "+CommandCancel.Slash())
_ = r.sendText(ctx, chatID, "*Amount exceeds allowed limit*\n\n*Max per operation:* "+markdownCode(typed.LimitMax())+"\n\nEnter another amount or "+markdownCommand(CommandCancel)+".")
return
case "daily":
_ = r.sendText(ctx, chatID, "Daily amount limit exceeded.\n\nMax per day: "+typed.LimitMax()+"\n\nEnter another amount or "+CommandCancel.Slash())
_ = r.sendText(ctx, chatID, "*Daily amount limit exceeded*\n\n*Max per day:* "+markdownCode(typed.LimitMax())+"\n\nEnter another amount or "+markdownCommand(CommandCancel)+".")
return
}
}
if errors.Is(err, merrors.ErrInvalidArg) {
_ = r.sendText(ctx, chatID, "Invalid amount.\n\n"+amountInputHint+"\n\nEnter another amount or "+CommandCancel.Slash())
_ = r.sendText(ctx, chatID, "*Invalid amount*\n\n"+amountInputHint+"\n\nEnter another amount or "+markdownCommand(CommandCancel)+".")
return
}
_ = r.sendText(ctx, chatID, "Failed to create treasury request.\n\nEnter another amount or "+CommandCancel.Slash())
_ = r.sendText(ctx, chatID, "*Failed to create treasury request*\n\nEnter another amount or "+markdownCommand(CommandCancel)+".")
return
}
if record == nil {
_ = r.sendText(ctx, chatID, "Failed to create treasury request.\n\nEnter another amount or "+CommandCancel.Slash())
_ = r.sendText(ctx, chatID, "*Failed to create treasury request*\n\nEnter another amount or "+markdownCommand(CommandCancel)+".")
return
}
r.dialogs.Set(userID, DialogSession{
@@ -311,12 +311,12 @@ func (r *Router) confirm(ctx context.Context, userID string, accountID string, c
}
}
if requestID == "" {
_ = r.sendText(ctx, chatID, "No pending treasury operation.")
_ = r.sendText(ctx, chatID, "*No pending treasury operation.*")
return
}
record, err := r.service.ConfirmRequest(ctx, requestID, userID)
if err != nil {
_ = r.sendText(ctx, chatID, "Unable to confirm treasury request.\n\nUse "+CommandCancel.Slash()+" or create a new request with "+CommandFund.Slash()+" or "+CommandWithdraw.Slash()+".")
_ = r.sendText(ctx, chatID, "*Unable to confirm treasury request.*\n\nUse "+markdownCommand(CommandCancel)+" or create a new request with "+markdownCommand(CommandFund)+" or "+markdownCommand(CommandWithdraw)+".")
return
}
if r.tracker != nil {
@@ -327,7 +327,12 @@ func (r *Router) confirm(ctx context.Context, userID string, accountID string, c
if delay < 0 {
delay = 0
}
_ = r.sendText(ctx, chatID, "Operation confirmed.\n\nExecution scheduled in "+formatSeconds(delay)+".\nYou can cancel during this cooldown with "+CommandCancel.Slash()+".\n\nYou will receive a follow-up message with execution success or failure.\n\nRequest ID: "+strings.TrimSpace(record.RequestID))
_ = r.sendText(ctx, chatID,
"*Operation confirmed*\n\n"+
"*Execution:* scheduled in "+markdownCode(formatSeconds(delay))+".\n"+
"You can cancel during this cooldown with "+markdownCommand(CommandCancel)+".\n\n"+
"You will receive a follow-up message with execution success or failure.\n\n"+
"*Request ID:* "+markdownCode(strings.TrimSpace(record.RequestID)))
}
func (r *Router) cancel(ctx context.Context, userID string, accountID string, chatID string) {
@@ -342,19 +347,19 @@ func (r *Router) cancel(ctx context.Context, userID string, accountID string, ch
}
if requestID == "" {
r.dialogs.Clear(userID)
_ = r.sendText(ctx, chatID, "No pending treasury operation.")
_ = r.sendText(ctx, chatID, "*No pending treasury operation.*")
return
}
record, err := r.service.CancelRequest(ctx, requestID, userID)
if err != nil {
_ = r.sendText(ctx, chatID, "Unable to cancel treasury request.")
_ = r.sendText(ctx, chatID, "*Unable to cancel treasury request.*")
return
}
if r.tracker != nil {
r.tracker.Untrack(record.RequestID)
}
r.dialogs.Clear(userID)
_ = r.sendText(ctx, chatID, "Operation cancelled.\n\nRequest ID: "+strings.TrimSpace(record.RequestID))
_ = r.sendText(ctx, chatID, "*Operation cancelled*\n\n*Request ID:* "+markdownCode(strings.TrimSpace(record.RequestID)))
}
func (r *Router) sendText(ctx context.Context, chatID string, text string) error {
@@ -394,27 +399,27 @@ func (r *Router) logUnauthorized(update *model.TelegramWebhookUpdate) {
func pendingRequestMessage(record *storagemodel.TreasuryRequest) string {
if record == nil {
return "You already have a pending treasury operation.\n\n" + CommandCancel.Slash()
return "*Pending treasury operation already exists.*\n\nUse " + markdownCommand(CommandCancel) + "."
}
return "You already have a pending treasury operation.\n\n" +
"Account: " + requestAccountDisplay(record) + "\n" +
"Request ID: " + strings.TrimSpace(record.RequestID) + "\n" +
"Status: " + strings.TrimSpace(string(record.Status)) + "\n" +
"Amount: " + strings.TrimSpace(record.Amount) + " " + strings.TrimSpace(record.Currency) + "\n\n" +
"Wait for execution or cancel it.\n\n" + CommandCancel.Slash()
return "*Pending Treasury Operation*\n\n" +
"*Account:* " + markdownCode(requestAccountDisplay(record)) + "\n" +
"*Request ID:* " + markdownCode(strings.TrimSpace(record.RequestID)) + "\n" +
"*Status:* " + markdownCode(strings.TrimSpace(string(record.Status))) + "\n" +
"*Amount:* " + markdownCode(strings.TrimSpace(record.Amount)+" "+strings.TrimSpace(record.Currency)) + "\n\n" +
"Wait for execution or cancel with " + markdownCommand(CommandCancel) + "."
}
func confirmationPrompt(record *storagemodel.TreasuryRequest) string {
if record == nil {
return "Request created.\n\n" + CommandConfirm.Slash() + "\n" + CommandCancel.Slash()
return "*Request created.*\n\nUse " + markdownCommand(CommandConfirm) + " or " + markdownCommand(CommandCancel) + "."
}
title := "Funding request created."
title := "*Funding request created.*"
if record.OperationType == storagemodel.TreasuryOperationWithdraw {
title = "Withdrawal request created."
title = "*Withdrawal request created.*"
}
return title + "\n\n" +
"Account: " + requestAccountDisplay(record) + "\n" +
"Amount: " + strings.TrimSpace(record.Amount) + " " + strings.TrimSpace(record.Currency) + "\n\n" +
"*Account:* " + markdownCode(requestAccountDisplay(record)) + "\n" +
"*Amount:* " + markdownCode(strings.TrimSpace(record.Amount)+" "+strings.TrimSpace(record.Currency)) + "\n\n" +
confirmationCommandsMessage()
}
@@ -430,13 +435,17 @@ func welcomeMessage(profile *AccountProfile) string {
if currency == "" {
currency = "N/A"
}
return "Welcome to Sendico treasury bot.\n\nAttached account: " + accountCode + " (" + currency + ").\nUse " + CommandFund.Slash() + " to credit your account and " + CommandWithdraw.Slash() + " to debit it.\nAfter entering an amount, use " + CommandConfirm.Slash() + " or " + CommandCancel.Slash() + ".\nUse " + CommandHelp.Slash() + " for detailed usage."
return "*Sendico Treasury Bot*\n\n" +
"*Attached account:* " + markdownCode(accountCode) + " (" + markdownCode(currency) + ")\n" +
"Use " + markdownCommand(CommandFund) + " to credit your account and " + markdownCommand(CommandWithdraw) + " to debit it.\n" +
"After entering an amount, use " + markdownCommand(CommandConfirm) + " or " + markdownCommand(CommandCancel) + ".\n" +
"Use " + markdownCommand(CommandHelp) + " for detailed usage."
}
func amountPromptMessage(operation storagemodel.TreasuryOperationType, profile *AccountProfile, fallbackAccountID string) string {
action := "fund"
title := "*Funding request*"
if operation == storagemodel.TreasuryOperationWithdraw {
action = "withdraw"
title = "*Withdrawal request*"
}
accountCode := displayAccountCode(profile, fallbackAccountID)
currency := ""
@@ -449,7 +458,9 @@ func amountPromptMessage(operation storagemodel.TreasuryOperationType, profile *
if currency == "" {
currency = "N/A"
}
return "Preparing to " + action + " account " + accountCode + " (" + currency + ").\n\n" + amountInputHint
return title + "\n\n" +
"*Account:* " + markdownCode(accountCode) + " (" + markdownCode(currency) + ")\n\n" +
amountInputHint
}
func requestAccountDisplay(record *storagemodel.TreasuryRequest) string {

View File

@@ -272,11 +272,11 @@ func executionMessage(result *ExecutionResult) string {
balanceCurrency = strings.TrimSpace(result.NewBalance.Currency)
}
}
return op + " completed.\n\n" +
"Account: " + requestAccountCode(request) + "\n" +
"Amount: " + sign + strings.TrimSpace(request.Amount) + " " + strings.TrimSpace(request.Currency) + "\n" +
"New balance: " + balanceAmount + " " + balanceCurrency + "\n\n" +
"Reference: " + strings.TrimSpace(request.RequestID)
return "*" + op + " completed*\n\n" +
"*Account:* " + markdownCode(requestAccountCode(request)) + "\n" +
"*Amount:* " + markdownCode(sign+strings.TrimSpace(request.Amount)+" "+strings.TrimSpace(request.Currency)) + "\n" +
"*New balance:* " + markdownCode(balanceAmount+" "+balanceCurrency) + "\n\n" +
"*Reference:* " + markdownCode(strings.TrimSpace(request.RequestID))
case storagemodel.TreasuryRequestStatusFailed:
reason := strings.TrimSpace(request.ErrorMessage)
if reason == "" && result.ExecutionError != nil {
@@ -285,12 +285,12 @@ func executionMessage(result *ExecutionResult) string {
if reason == "" {
reason = "Unknown error."
}
return "Execution failed.\n\n" +
"Account: " + requestAccountCode(request) + "\n" +
"Amount: " + strings.TrimSpace(request.Amount) + " " + strings.TrimSpace(request.Currency) + "\n" +
"Status: FAILED\n\n" +
"Reason:\n" + reason + "\n\n" +
"Request ID: " + strings.TrimSpace(request.RequestID)
return "*Execution failed*\n\n" +
"*Account:* " + markdownCode(requestAccountCode(request)) + "\n" +
"*Amount:* " + markdownCode(strings.TrimSpace(request.Amount)+" "+strings.TrimSpace(request.Currency)) + "\n" +
"*Status:* " + markdownCode("FAILED") + "\n" +
"*Reason:* " + markdownCode(compactForMarkdown(reason)) + "\n\n" +
"*Request ID:* " + markdownCode(strings.TrimSpace(request.RequestID))
default:
return ""
}
@@ -305,3 +305,23 @@ func requestAccountCode(request *storagemodel.TreasuryRequest) string {
}
return strings.TrimSpace(request.LedgerAccountID)
}
func markdownCode(value string) string {
value = strings.TrimSpace(value)
if value == "" {
value = "N/A"
}
value = strings.ReplaceAll(value, "`", "'")
return "`" + value + "`"
}
func compactForMarkdown(value string) string {
value = strings.TrimSpace(value)
if value == "" {
return "Unknown error."
}
value = strings.ReplaceAll(value, "\r\n", " ")
value = strings.ReplaceAll(value, "\n", " ")
value = strings.ReplaceAll(value, "\r", " ")
return strings.Join(strings.Fields(value), " ")
}