extended confirmation flow handling #538

Merged
tech merged 1 commits from tg-537 into main 2026-02-19 20:08:42 +00:00
2 changed files with 74 additions and 17 deletions
Showing only changes of commit 2bf05ab414 - Show all commits

View File

@@ -148,14 +148,23 @@ func (s *Service) onTelegramUpdate(ctx context.Context, update *model.TelegramWe
if replyToID == "" { if replyToID == "" {
return nil return nil
} }
replyFields := telegramReplyLogFields(update)
pending, err := s.repo.PendingConfirmations().FindByMessageID(ctx, replyToID) pending, err := s.repo.PendingConfirmations().FindByMessageID(ctx, replyToID)
if err != nil { if err != nil {
return err return err
} }
if pending == nil { if pending == nil {
s.logger.Debug("Telegram reply ignored: no pending confirmation for message", zap.String("reply_to_message_id", replyToID), zap.Int64("update_id", update.UpdateID)) s.logger.Info("Telegram confirmation reply dropped",
append(replyFields,
zap.String("outcome", "dropped"),
zap.String("reason", "no_pending_confirmation"),
)...)
return nil return nil
} }
replyFields = append(replyFields,
zap.String("request_id", strings.TrimSpace(pending.RequestID)),
zap.String("target_chat_id", strings.TrimSpace(pending.TargetChatID)),
)
if !pending.ExpiresAt.IsZero() && time.Now().After(pending.ExpiresAt) { if !pending.ExpiresAt.IsZero() && time.Now().After(pending.ExpiresAt) {
result := &model.ConfirmationResult{ result := &model.ConfirmationResult{
@@ -165,14 +174,25 @@ func (s *Service) onTelegramUpdate(ctx context.Context, update *model.TelegramWe
if err := s.publishPendingConfirmationResult(pending, result); err != nil { if err := s.publishPendingConfirmationResult(pending, result); err != nil {
return err return err
} }
return s.clearPendingConfirmation(ctx, pending.RequestID) if err := s.clearPendingConfirmation(ctx, pending.RequestID); err != nil {
return err
}
s.logger.Info("Telegram confirmation reply processed",
append(replyFields,
zap.String("outcome", "processed"),
zap.String("result_status", string(result.Status)),
zap.String("reason", "expired_confirmation"),
)...)
return nil
} }
if strings.TrimSpace(message.ChatID) != strings.TrimSpace(pending.TargetChatID) { if strings.TrimSpace(message.ChatID) != strings.TrimSpace(pending.TargetChatID) {
s.logger.Debug("Telegram reply ignored: chat mismatch", s.logger.Info("Telegram confirmation reply dropped",
zap.String("request_id", pending.RequestID), append(replyFields,
zap.String("expected_chat_id", pending.TargetChatID), zap.String("outcome", "dropped"),
zap.String("chat_id", strings.TrimSpace(message.ChatID))) zap.String("reason", "chat_mismatch"),
zap.String("expected_chat_id", strings.TrimSpace(pending.TargetChatID)),
)...)
return nil return nil
} }
@@ -192,7 +212,16 @@ func (s *Service) onTelegramUpdate(ctx context.Context, update *model.TelegramWe
ReplyToMessageID: message.MessageID, ReplyToMessageID: message.MessageID,
Text: "Only approved users can confirm this payment.", Text: "Only approved users can confirm this payment.",
}) })
return s.clearPendingConfirmation(ctx, pending.RequestID) if err := s.clearPendingConfirmation(ctx, pending.RequestID); err != nil {
return err
}
s.logger.Info("Telegram confirmation reply processed",
append(replyFields,
zap.String("outcome", "processed"),
zap.String("result_status", string(result.Status)),
zap.String("reason", "unauthorized_user"),
)...)
return nil
} }
money, reason, err := parseConfirmationReply(message.Text) money, reason, err := parseConfirmationReply(message.Text)
@@ -206,6 +235,12 @@ func (s *Service) onTelegramUpdate(ctx context.Context, update *model.TelegramWe
ReplyToMessageID: message.MessageID, ReplyToMessageID: message.MessageID,
Text: clarificationMessage(reason), Text: clarificationMessage(reason),
}) })
s.logger.Info("Telegram confirmation reply dropped",
append(replyFields,
zap.String("outcome", "dropped"),
zap.String("reason", "invalid_reply_format"),
zap.String("parse_reason", reason),
)...)
return nil return nil
} }
@@ -222,7 +257,29 @@ func (s *Service) onTelegramUpdate(ctx context.Context, update *model.TelegramWe
if err := s.publishPendingConfirmationResult(pending, result); err != nil { if err := s.publishPendingConfirmationResult(pending, result); err != nil {
return err return err
} }
return s.clearPendingConfirmation(ctx, pending.RequestID) if err := s.clearPendingConfirmation(ctx, pending.RequestID); err != nil {
return err
}
s.logger.Info("Telegram confirmation reply processed",
append(replyFields,
zap.String("outcome", "processed"),
zap.String("result_status", string(result.Status)),
)...)
return nil
}
func telegramReplyLogFields(update *model.TelegramWebhookUpdate) []zap.Field {
if update == nil || update.Message == nil {
return nil
}
message := update.Message
return []zap.Field{
zap.Int64("update_id", update.UpdateID),
zap.String("message_id", strings.TrimSpace(message.MessageID)),
zap.String("reply_to_message_id", strings.TrimSpace(message.ReplyToMessageID)),
zap.String("chat_id", strings.TrimSpace(message.ChatID)),
zap.String("from_user_id", strings.TrimSpace(message.FromUserID)),
}
} }
func (s *Service) publishPendingConfirmationResult(pending *storagemodel.PendingConfirmation, result *model.ConfirmationResult) error { func (s *Service) publishPendingConfirmationResult(pending *storagemodel.PendingConfirmation, result *model.ConfirmationResult) error {

View File

@@ -10,15 +10,15 @@ import (
// OrchestrationStep defines a template step for execution planning. // OrchestrationStep defines a template step for execution planning.
type OrchestrationStep struct { type OrchestrationStep struct {
StepID string `bson:"stepId" json:"stepId"` StepID string `bson:"stepId" json:"stepId"`
Rail Rail `bson:"rail" json:"rail"` Rail Rail `bson:"rail" json:"rail"`
Operation string `bson:"operation" json:"operation"` Operation string `bson:"operation" json:"operation"`
ReportVisibility ReportVisibility `bson:"reportVisibility,omitempty" json:"reportVisibility,omitempty"` ReportVisibility ReportVisibility `bson:"reportVisibility,omitempty" json:"reportVisibility,omitempty"`
DependsOn []string `bson:"dependsOn,omitempty" json:"dependsOn,omitempty"` DependsOn []string `bson:"dependsOn,omitempty" json:"dependsOn,omitempty"`
CommitPolicy CommitPolicy `bson:"commitPolicy,omitempty" json:"commitPolicy,omitempty"` CommitPolicy CommitPolicy `bson:"commitPolicy,omitempty" json:"commitPolicy,omitempty"`
CommitAfter []string `bson:"commitAfter,omitempty" json:"commitAfter,omitempty"` CommitAfter []string `bson:"commitAfter,omitempty" json:"commitAfter,omitempty"`
FromRole *account_role.AccountRole `bson:"fromRole,omitempty" json:"fromRole,omitempty"` FromRole *account_role.AccountRole `bson:"fromRole,omitempty" json:"fromRole,omitempty"`
ToRole *account_role.AccountRole `bson:"toRole,omitempty" json:"toRole,omitempty"` ToRole *account_role.AccountRole `bson:"toRole,omitempty" json:"toRole,omitempty"`
} }
// PaymentPlanTemplate stores reusable orchestration templates. // PaymentPlanTemplate stores reusable orchestration templates.