new settlement flow

This commit is contained in:
Stephan D
2026-01-20 22:29:30 +01:00
parent ae6c617136
commit e0d7320fad
42 changed files with 428 additions and 73 deletions

View File

@@ -9,9 +9,9 @@ import (
"time"
"github.com/tech/sendico/notification/internal/server/notificationimp/telegram"
"github.com/tech/sendico/pkg/merrors"
msg "github.com/tech/sendico/pkg/messaging"
confirmations "github.com/tech/sendico/pkg/messaging/notifications/confirmations"
"github.com/tech/sendico/pkg/merrors"
"github.com/tech/sendico/pkg/mlogger"
"github.com/tech/sendico/pkg/model"
"github.com/tech/sendico/pkg/mservice"
@@ -35,12 +35,12 @@ type confirmationManager struct {
}
type confirmationState struct {
request model.ConfirmationRequest
requestMessageID string
targetChatID string
callbackSubject string
clarified bool
timer *time.Timer
request model.ConfirmationRequest
requestMessageID string
targetChatID string
callbackSubject string
clarified bool
timer *time.Timer
}
func newConfirmationManager(logger mlogger.Logger, tg telegram.Client, outbox msg.Producer) *confirmationManager {

View File

@@ -11,10 +11,11 @@ import (
"github.com/tech/sendico/pkg/domainprovider"
"github.com/tech/sendico/pkg/merrors"
na "github.com/tech/sendico/pkg/messaging/notifications/account"
confirmations "github.com/tech/sendico/pkg/messaging/notifications/confirmations"
cnotifications "github.com/tech/sendico/pkg/messaging/notifications/confirmation"
confirmations "github.com/tech/sendico/pkg/messaging/notifications/confirmations"
ni "github.com/tech/sendico/pkg/messaging/notifications/invitation"
snotifications "github.com/tech/sendico/pkg/messaging/notifications/site"
tnotifications "github.com/tech/sendico/pkg/messaging/notifications/telegram"
"github.com/tech/sendico/pkg/mlogger"
"github.com/tech/sendico/pkg/model"
"github.com/tech/sendico/pkg/mservice"
@@ -91,6 +92,10 @@ func CreateAPI(a api.API) (*NotificationAPI, error) {
p.logger.Error("Failed to register confirmation request handler", zap.Error(err))
return nil, err
}
if err := a.Register().Consumer(tnotifications.NewTelegramReactionProcessor(p.logger, p.onTelegramReaction)); err != nil {
p.logger.Error("Failed to register telegram reaction handler", zap.Error(err))
return nil, err
}
idb, err := a.DBFactory().NewInvitationsDB()
if err != nil {

View File

@@ -26,6 +26,7 @@ type Client interface {
SendContactRequest(ctx context.Context, request *model.ContactRequest) error
SendCallRequest(ctx context.Context, request *model.CallRequest) error
SendText(ctx context.Context, chatID string, text string, replyToMessageID string) (*model.TelegramMessage, error)
SetMessageReaction(ctx context.Context, chatID string, messageID string, emoji string) error
}
type client struct {
@@ -132,6 +133,25 @@ type messageUser struct {
Username string `json:"username,omitempty"`
}
type setMessageReactionPayload struct {
ChatID string `json:"chat_id"`
MessageID int64 `json:"message_id"`
Reaction []reactionType `json:"reaction,omitempty"`
IsBig bool `json:"is_big,omitempty"`
}
type reactionType struct {
Type string `json:"type"`
Emoji string `json:"emoji,omitempty"`
CustomEmojiID string `json:"custom_emoji_id,omitempty"`
}
type setMessageReactionResponse struct {
OK bool `json:"ok"`
Result bool `json:"result,omitempty"`
Description string `json:"description,omitempty"`
}
func (c *client) sendMessage(ctx context.Context, payload sendMessagePayload) (*model.TelegramMessage, error) {
body, err := json.Marshal(&payload)
if err != nil {
@@ -193,7 +213,12 @@ func (c *client) sendMessage(ctx context.Context, payload sendMessagePayload) (*
}
func (c *client) endpoint() string {
return fmt.Sprintf("%s/bot%s/sendMessage", c.apiURL, c.botToken)
return c.endpointFor("sendMessage")
}
func (c *client) endpointFor(method string) string {
method = strings.TrimPrefix(strings.TrimSpace(method), "/")
return fmt.Sprintf("%s/bot%s/%s", c.apiURL, c.botToken, method)
}
func (c *client) SendContactRequest(ctx context.Context, request *model.ContactRequest) error {
@@ -248,6 +273,80 @@ func (c *client) SendText(ctx context.Context, chatID string, text string, reply
return c.sendMessage(ctx, payload)
}
func (c *client) SetMessageReaction(ctx context.Context, chatID string, messageID string, emoji string) error {
chatID = strings.TrimSpace(chatID)
if chatID == "" {
return merrors.InvalidArgument("telegram chat id is empty", "chat_id")
}
msgID, err := strconv.ParseInt(strings.TrimSpace(messageID), 10, 64)
if err != nil {
return merrors.InvalidArgumentWrap(err, "invalid message_id", "message_id")
}
emoji = strings.TrimSpace(emoji)
reaction := []reactionType{}
if emoji != "" {
reaction = append(reaction, reactionType{Type: "emoji", Emoji: emoji})
}
payload := setMessageReactionPayload{
ChatID: chatID,
MessageID: msgID,
Reaction: reaction,
}
return c.sendReaction(ctx, payload)
}
func (c *client) sendReaction(ctx context.Context, payload setMessageReactionPayload) error {
body, err := json.Marshal(&payload)
if err != nil {
c.logger.Warn("Failed to marshal telegram reaction payload", zap.Error(err))
return err
}
req, err := http.NewRequestWithContext(ctx, http.MethodPost, c.endpointFor("setMessageReaction"), bytes.NewReader(body))
if err != nil {
c.logger.Warn("Failed to create telegram reaction request", zap.Error(err))
return err
}
req.Header.Set("Content-Type", "application/json")
fields := []zap.Field{
zap.String("chat_id", payload.ChatID),
zap.Int64("message_id", payload.MessageID),
zap.Int("payload_size_bytes", len(body)),
}
c.logger.Debug("Sending Telegram reaction", fields...)
start := time.Now()
resp, err := c.httpClient.Do(req)
if err != nil {
c.logger.Warn("Telegram reaction request failed", zap.Error(err))
return err
}
defer resp.Body.Close()
respBody, _ := io.ReadAll(io.LimitReader(resp.Body, 16<<10))
if resp.StatusCode >= http.StatusOK && resp.StatusCode < http.StatusMultipleChoices {
var parsed setMessageReactionResponse
if err := json.Unmarshal(respBody, &parsed); err != nil {
c.logger.Warn("Failed to decode telegram reaction response", zap.Error(err))
return err
}
if !parsed.OK || !parsed.Result {
msg := "telegram setMessageReaction response missing result"
if parsed.Description != "" {
msg = parsed.Description
}
return merrors.Internal(msg)
}
c.logger.Debug("Telegram reaction sent", zap.Int("status_code", resp.StatusCode), zap.Duration("latency", time.Since(start)))
return nil
}
c.logger.Warn("Telegram API returned non-success status for reaction",
zap.Int("status_code", resp.StatusCode),
zap.ByteString("response_body", respBody),
zap.String("chat_id", payload.ChatID))
return merrors.Internal(fmt.Sprintf("telegram setMessageReaction failed with status %d: %s", resp.StatusCode, string(respBody)))
}
func toTelegramMessage(msg *messageResponse) *model.TelegramMessage {
if msg == nil {
return nil

View File

@@ -0,0 +1,46 @@
package notificationimp
import (
"context"
"strings"
"github.com/tech/sendico/pkg/merrors"
"github.com/tech/sendico/pkg/model"
"go.uber.org/zap"
)
func (a *NotificationAPI) onTelegramReaction(ctx context.Context, request *model.TelegramReactionRequest) error {
if request == nil {
return merrors.InvalidArgument("telegram reaction request is nil", "request")
}
if a.tg == nil {
return merrors.Internal("telegram client is not configured")
}
chatID := strings.TrimSpace(request.ChatID)
messageID := strings.TrimSpace(request.MessageID)
if chatID == "" {
return merrors.InvalidArgument("telegram chat_id is required", "chat_id")
}
if messageID == "" {
return merrors.InvalidArgument("telegram message_id is required", "message_id")
}
emoji := strings.TrimSpace(request.Emoji)
if emoji == "" {
return merrors.InvalidArgument("telegram emoji is required", "emoji")
}
if err := a.tg.SetMessageReaction(ctx, chatID, messageID, emoji); err != nil {
a.logger.Warn("Failed to send telegram reaction",
zap.Error(err),
zap.String("request_id", strings.TrimSpace(request.RequestID)),
zap.String("chat_id", chatID),
zap.String("message_id", messageID),
zap.String("emoji", emoji))
return err
}
a.logger.Info("Telegram reaction sent",
zap.String("request_id", strings.TrimSpace(request.RequestID)),
zap.String("chat_id", chatID),
zap.String("message_id", messageID),
zap.String("emoji", emoji))
return nil
}