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

@@ -18,6 +18,7 @@ import (
confirmations "github.com/tech/sendico/pkg/messaging/notifications/confirmations"
paymentgateway "github.com/tech/sendico/pkg/messaging/notifications/paymentgateway"
np "github.com/tech/sendico/pkg/messaging/notifications/processor"
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"
@@ -34,8 +35,9 @@ import (
)
const (
defaultConfirmationTimeoutSeconds = 120
defaultConfirmationTimeoutSeconds = 259200
executedStatus = "executed"
telegramSuccessReaction = "\u2705"
)
const (
@@ -43,6 +45,8 @@ const (
metadataQuoteRef = "quote_ref"
metadataTargetChatID = "target_chat_id"
metadataOutgoingLeg = "outgoing_leg"
metadataSourceAmount = "source_amount"
metadataSourceCurrency = "source_currency"
)
type Config struct {
@@ -77,14 +81,14 @@ func NewService(logger mlogger.Logger, repo storage.Repository, producer msg.Pro
}
logger = logger.Named("service")
svc := &Service{
logger: logger,
repo: repo,
producer: producer,
broker: broker,
cfg: cfg,
rail: strings.TrimSpace(cfg.Rail),
logger: logger,
repo: repo,
producer: producer,
broker: broker,
cfg: cfg,
rail: strings.TrimSpace(cfg.Rail),
invokeURI: strings.TrimSpace(cfg.InvokeURI),
pending: map[string]*model.PaymentGatewayIntent{},
pending: map[string]*model.PaymentGatewayIntent{},
}
svc.chatID = strings.TrimSpace(readEnv(cfg.TargetChatIDEnv))
svc.startConsumers()
@@ -322,6 +326,7 @@ func (s *Service) onConfirmationResult(ctx context.Context, result *model.Confir
}
s.publishExecution(intent, result)
s.publishTelegramReaction(result)
s.removeIntent(requestID)
return nil
}
@@ -416,6 +421,39 @@ func (s *Service) publishExecution(intent *model.PaymentGatewayIntent, result *m
zap.String("status", string(result.Status)))
}
func (s *Service) publishTelegramReaction(result *model.ConfirmationResult) {
if s == nil || s.producer == nil || result == nil || result.RawReply == nil {
return
}
if result.Status != model.ConfirmationStatusConfirmed && result.Status != model.ConfirmationStatusClarified {
return
}
request := &model.TelegramReactionRequest{
RequestID: strings.TrimSpace(result.RequestID),
ChatID: strings.TrimSpace(result.RawReply.ChatID),
MessageID: strings.TrimSpace(result.RawReply.MessageID),
Emoji: telegramSuccessReaction,
}
if request.ChatID == "" || request.MessageID == "" || request.Emoji == "" {
return
}
env := tnotifications.TelegramReaction(string(mservice.PaymentGateway), request)
if err := s.producer.SendMessage(env); err != nil {
s.logger.Warn("Failed to publish telegram reaction",
zap.Error(err),
zap.String("request_id", request.RequestID),
zap.String("chat_id", request.ChatID),
zap.String("message_id", request.MessageID),
zap.String("emoji", request.Emoji))
return
}
s.logger.Info("Published telegram reaction",
zap.String("request_id", request.RequestID),
zap.String("chat_id", request.ChatID),
zap.String("message_id", request.MessageID),
zap.String("emoji", request.Emoji))
}
func (s *Service) trackIntent(requestID string, intent *model.PaymentGatewayIntent) {
if s == nil || intent == nil {
return
@@ -507,6 +545,7 @@ func intentFromSubmitTransfer(req *chainv1.SubmitTransferRequest, defaultRail, d
if amount == nil {
return nil, merrors.InvalidArgument("submit_transfer: amount is required")
}
metadata := req.GetMetadata()
requestedMoney := &paymenttypes.Money{
Amount: strings.TrimSpace(amount.GetAmount()),
Currency: strings.TrimSpace(amount.GetCurrency()),
@@ -514,7 +553,14 @@ func intentFromSubmitTransfer(req *chainv1.SubmitTransferRequest, defaultRail, d
if requestedMoney.Amount == "" || requestedMoney.Currency == "" {
return nil, merrors.InvalidArgument("submit_transfer: amount is required")
}
metadata := req.GetMetadata()
sourceAmount := strings.TrimSpace(metadata[metadataSourceAmount])
sourceCurrency := strings.TrimSpace(metadata[metadataSourceCurrency])
if sourceAmount != "" && sourceCurrency != "" {
requestedMoney = &paymenttypes.Money{
Amount: sourceAmount,
Currency: sourceCurrency,
}
}
paymentIntentID := strings.TrimSpace(req.GetClientReference())
if paymentIntentID == "" {
paymentIntentID = strings.TrimSpace(metadata[metadataPaymentIntentID])

View File

@@ -9,11 +9,13 @@ import (
"github.com/tech/sendico/gateway/tgsettle/storage"
storagemodel "github.com/tech/sendico/gateway/tgsettle/storage/model"
envelope "github.com/tech/sendico/pkg/messaging/envelope"
mloggerfactory "github.com/tech/sendico/pkg/mlogger/factory"
"github.com/tech/sendico/pkg/model"
notification "github.com/tech/sendico/pkg/model/notification"
"github.com/tech/sendico/pkg/mservice"
mloggerfactory "github.com/tech/sendico/pkg/mlogger/factory"
paymenttypes "github.com/tech/sendico/pkg/payments/types"
moneyv1 "github.com/tech/sendico/pkg/proto/common/money/v1"
chainv1 "github.com/tech/sendico/pkg/proto/gateway/chain/v1"
)
type fakePaymentsStore struct {
@@ -147,6 +149,25 @@ func TestOnIntentCreatesConfirmationRequest(t *testing.T) {
}
}
func TestIntentFromSubmitTransferUsesSourceMoney(t *testing.T) {
req := &chainv1.SubmitTransferRequest{
IdempotencyKey: "idem-1",
ClientReference: "pi-1",
Amount: &moneyv1.Money{Amount: "10.00", Currency: "EUR"},
Metadata: map[string]string{
metadataSourceAmount: "12.34",
metadataSourceCurrency: "USD",
},
}
intent, err := intentFromSubmitTransfer(req, "provider_settlement", "")
if err != nil {
t.Fatalf("intentFromSubmitTransfer error: %v", err)
}
if intent.RequestedMoney == nil || intent.RequestedMoney.Amount != "12.34" || intent.RequestedMoney.Currency != "USD" {
t.Fatalf("expected source money override, got: %#v", intent.RequestedMoney)
}
}
func TestConfirmationResultPersistsExecutionAndReply(t *testing.T) {
logger := mloggerfactory.NewLogger(false)
repo := &fakeRepo{payments: &fakePaymentsStore{}, tg: &fakeTelegramStore{}}