290 lines
9.6 KiB
Go
290 lines
9.6 KiB
Go
package gateway
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"sync"
|
|
"testing"
|
|
|
|
"github.com/tech/sendico/gateway/tgsettle/storage"
|
|
storagemodel "github.com/tech/sendico/gateway/tgsettle/storage/model"
|
|
envelope "github.com/tech/sendico/pkg/messaging/envelope"
|
|
"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"
|
|
)
|
|
|
|
type fakePaymentsStore struct {
|
|
mu sync.Mutex
|
|
executions map[string]*storagemodel.PaymentExecution
|
|
}
|
|
|
|
func (f *fakePaymentsStore) FindByIdempotencyKey(_ context.Context, key string) (*storagemodel.PaymentExecution, error) {
|
|
f.mu.Lock()
|
|
defer f.mu.Unlock()
|
|
return f.executions[key], nil
|
|
}
|
|
|
|
func (f *fakePaymentsStore) InsertExecution(_ context.Context, exec *storagemodel.PaymentExecution) error {
|
|
f.mu.Lock()
|
|
defer f.mu.Unlock()
|
|
if f.executions == nil {
|
|
f.executions = map[string]*storagemodel.PaymentExecution{}
|
|
}
|
|
if _, ok := f.executions[exec.IdempotencyKey]; ok {
|
|
return storage.ErrDuplicate
|
|
}
|
|
f.executions[exec.IdempotencyKey] = exec
|
|
return nil
|
|
}
|
|
|
|
type fakeTelegramStore struct {
|
|
mu sync.Mutex
|
|
records map[string]*storagemodel.TelegramConfirmation
|
|
}
|
|
|
|
func (f *fakeTelegramStore) Upsert(_ context.Context, record *storagemodel.TelegramConfirmation) error {
|
|
f.mu.Lock()
|
|
defer f.mu.Unlock()
|
|
if f.records == nil {
|
|
f.records = map[string]*storagemodel.TelegramConfirmation{}
|
|
}
|
|
f.records[record.RequestID] = record
|
|
return nil
|
|
}
|
|
|
|
type fakeRepo struct {
|
|
payments *fakePaymentsStore
|
|
tg *fakeTelegramStore
|
|
}
|
|
|
|
func (f *fakeRepo) Payments() storage.PaymentsStore {
|
|
return f.payments
|
|
}
|
|
|
|
func (f *fakeRepo) TelegramConfirmations() storage.TelegramConfirmationsStore {
|
|
return f.tg
|
|
}
|
|
|
|
type captureProducer struct {
|
|
mu sync.Mutex
|
|
confirmationRequests []*model.ConfirmationRequest
|
|
executions []*model.PaymentGatewayExecution
|
|
}
|
|
|
|
func (c *captureProducer) SendMessage(env envelope.Envelope) error {
|
|
_, _ = env.Serialize()
|
|
switch env.GetSignature().ToString() {
|
|
case model.NewNotification(mservice.Notifications, notification.NAConfirmationRequest).ToString():
|
|
var req model.ConfirmationRequest
|
|
if err := json.Unmarshal(env.GetData(), &req); err == nil {
|
|
c.mu.Lock()
|
|
c.confirmationRequests = append(c.confirmationRequests, &req)
|
|
c.mu.Unlock()
|
|
}
|
|
case model.NewNotification(mservice.PaymentGateway, notification.NAPaymentGatewayExecution).ToString():
|
|
var exec model.PaymentGatewayExecution
|
|
if err := json.Unmarshal(env.GetData(), &exec); err == nil {
|
|
c.mu.Lock()
|
|
c.executions = append(c.executions, &exec)
|
|
c.mu.Unlock()
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (c *captureProducer) Reset() {
|
|
c.mu.Lock()
|
|
defer c.mu.Unlock()
|
|
c.confirmationRequests = nil
|
|
c.executions = nil
|
|
}
|
|
|
|
func TestOnIntentCreatesConfirmationRequest(t *testing.T) {
|
|
logger := mloggerfactory.NewLogger(false)
|
|
repo := &fakeRepo{payments: &fakePaymentsStore{}, tg: &fakeTelegramStore{}}
|
|
prod := &captureProducer{}
|
|
t.Setenv("PGS_CHAT_ID", "-100")
|
|
svc := NewService(logger, repo, prod, nil, Config{
|
|
Rail: "card",
|
|
TargetChatIDEnv: "PGS_CHAT_ID",
|
|
TimeoutSeconds: 90,
|
|
AcceptedUserIDs: []string{"42"},
|
|
})
|
|
prod.Reset()
|
|
|
|
intent := &model.PaymentGatewayIntent{
|
|
PaymentIntentID: "pi-1",
|
|
IdempotencyKey: "idem-1",
|
|
OutgoingLeg: "card",
|
|
QuoteRef: "quote-1",
|
|
RequestedMoney: &paymenttypes.Money{Amount: "10.50", Currency: "USD"},
|
|
TargetChatID: "",
|
|
}
|
|
if err := svc.onIntent(context.Background(), intent); err != nil {
|
|
t.Fatalf("onIntent error: %v", err)
|
|
}
|
|
if len(prod.confirmationRequests) != 1 {
|
|
t.Fatalf("expected 1 confirmation request, got %d", len(prod.confirmationRequests))
|
|
}
|
|
req := prod.confirmationRequests[0]
|
|
if req.RequestID != "idem-1" || req.PaymentIntentID != "pi-1" || req.QuoteRef != "quote-1" {
|
|
t.Fatalf("unexpected confirmation request fields: %#v", req)
|
|
}
|
|
if req.TargetChatID != "-100" {
|
|
t.Fatalf("expected target chat id -100, got %q", req.TargetChatID)
|
|
}
|
|
if req.RequestedMoney == nil || req.RequestedMoney.Amount != "10.50" || req.RequestedMoney.Currency != "USD" {
|
|
t.Fatalf("requested money mismatch: %#v", req.RequestedMoney)
|
|
}
|
|
if req.TimeoutSeconds != 90 {
|
|
t.Fatalf("expected timeout 90, got %d", req.TimeoutSeconds)
|
|
}
|
|
if req.SourceService != string(mservice.PaymentGateway) || req.Rail != "card" {
|
|
t.Fatalf("unexpected source/rail: %#v", req)
|
|
}
|
|
}
|
|
|
|
func TestConfirmationResultPersistsExecutionAndReply(t *testing.T) {
|
|
logger := mloggerfactory.NewLogger(false)
|
|
repo := &fakeRepo{payments: &fakePaymentsStore{}, tg: &fakeTelegramStore{}}
|
|
prod := &captureProducer{}
|
|
svc := NewService(logger, repo, prod, nil, Config{Rail: "card"})
|
|
intent := &model.PaymentGatewayIntent{
|
|
PaymentIntentID: "pi-2",
|
|
IdempotencyKey: "idem-2",
|
|
QuoteRef: "quote-2",
|
|
OutgoingLeg: "card",
|
|
RequestedMoney: &paymenttypes.Money{Amount: "5", Currency: "EUR"},
|
|
}
|
|
svc.trackIntent("idem-2", intent)
|
|
|
|
result := &model.ConfirmationResult{
|
|
RequestID: "idem-2",
|
|
Money: &paymenttypes.Money{Amount: "5", Currency: "EUR"},
|
|
Status: model.ConfirmationStatusConfirmed,
|
|
RawReply: &model.TelegramMessage{ChatID: "1", MessageID: "2", Text: "5 EUR"},
|
|
}
|
|
if err := svc.onConfirmationResult(context.Background(), result); err != nil {
|
|
t.Fatalf("onConfirmationResult error: %v", err)
|
|
}
|
|
if repo.payments.executions["idem-2"] == nil {
|
|
t.Fatalf("expected payment execution to be stored")
|
|
}
|
|
if repo.payments.executions["idem-2"].ExecutedMoney == nil || repo.payments.executions["idem-2"].ExecutedMoney.Amount != "5" {
|
|
t.Fatalf("executed money not stored correctly")
|
|
}
|
|
if repo.tg.records["idem-2"] == nil || repo.tg.records["idem-2"].RawReply.Text != "5 EUR" {
|
|
t.Fatalf("telegram reply not stored correctly")
|
|
}
|
|
}
|
|
|
|
func TestClarifiedResultPersistsExecution(t *testing.T) {
|
|
logger := mloggerfactory.NewLogger(false)
|
|
repo := &fakeRepo{payments: &fakePaymentsStore{}, tg: &fakeTelegramStore{}}
|
|
prod := &captureProducer{}
|
|
svc := NewService(logger, repo, prod, nil, Config{Rail: "card"})
|
|
intent := &model.PaymentGatewayIntent{
|
|
PaymentIntentID: "pi-clarified",
|
|
IdempotencyKey: "idem-clarified",
|
|
QuoteRef: "quote-clarified",
|
|
OutgoingLeg: "card",
|
|
RequestedMoney: &paymenttypes.Money{Amount: "12", Currency: "USD"},
|
|
}
|
|
svc.trackIntent("idem-clarified", intent)
|
|
|
|
result := &model.ConfirmationResult{
|
|
RequestID: "idem-clarified",
|
|
Money: &paymenttypes.Money{Amount: "12", Currency: "USD"},
|
|
Status: model.ConfirmationStatusClarified,
|
|
RawReply: &model.TelegramMessage{ChatID: "1", MessageID: "3", Text: "12 USD"},
|
|
}
|
|
if err := svc.onConfirmationResult(context.Background(), result); err != nil {
|
|
t.Fatalf("onConfirmationResult error: %v", err)
|
|
}
|
|
if repo.payments.executions["idem-clarified"] == nil {
|
|
t.Fatalf("expected payment execution to be stored")
|
|
}
|
|
}
|
|
|
|
func TestIdempotencyPreventsDuplicateWrites(t *testing.T) {
|
|
logger := mloggerfactory.NewLogger(false)
|
|
repo := &fakeRepo{payments: &fakePaymentsStore{executions: map[string]*storagemodel.PaymentExecution{
|
|
"idem-3": {IdempotencyKey: "idem-3"},
|
|
}}, tg: &fakeTelegramStore{}}
|
|
prod := &captureProducer{}
|
|
svc := NewService(logger, repo, prod, nil, Config{Rail: "card"})
|
|
intent := &model.PaymentGatewayIntent{
|
|
PaymentIntentID: "pi-3",
|
|
IdempotencyKey: "idem-3",
|
|
OutgoingLeg: "card",
|
|
QuoteRef: "quote-3",
|
|
RequestedMoney: &paymenttypes.Money{Amount: "1", Currency: "USD"},
|
|
TargetChatID: "chat",
|
|
}
|
|
if err := svc.onIntent(context.Background(), intent); err != nil {
|
|
t.Fatalf("onIntent error: %v", err)
|
|
}
|
|
if len(prod.confirmationRequests) != 0 {
|
|
t.Fatalf("expected no confirmation request for duplicate intent")
|
|
}
|
|
}
|
|
|
|
func TestTimeoutDoesNotPersistExecution(t *testing.T) {
|
|
logger := mloggerfactory.NewLogger(false)
|
|
repo := &fakeRepo{payments: &fakePaymentsStore{}, tg: &fakeTelegramStore{}}
|
|
prod := &captureProducer{}
|
|
svc := NewService(logger, repo, prod, nil, Config{Rail: "card"})
|
|
intent := &model.PaymentGatewayIntent{
|
|
PaymentIntentID: "pi-4",
|
|
IdempotencyKey: "idem-4",
|
|
QuoteRef: "quote-4",
|
|
OutgoingLeg: "card",
|
|
RequestedMoney: &paymenttypes.Money{Amount: "8", Currency: "USD"},
|
|
}
|
|
svc.trackIntent("idem-4", intent)
|
|
|
|
result := &model.ConfirmationResult{
|
|
RequestID: "idem-4",
|
|
Status: model.ConfirmationStatusTimeout,
|
|
}
|
|
if err := svc.onConfirmationResult(context.Background(), result); err != nil {
|
|
t.Fatalf("onConfirmationResult error: %v", err)
|
|
}
|
|
if repo.payments.executions["idem-4"] != nil {
|
|
t.Fatalf("expected no execution record for timeout")
|
|
}
|
|
}
|
|
|
|
func TestRejectedDoesNotPersistExecution(t *testing.T) {
|
|
logger := mloggerfactory.NewLogger(false)
|
|
repo := &fakeRepo{payments: &fakePaymentsStore{}, tg: &fakeTelegramStore{}}
|
|
prod := &captureProducer{}
|
|
svc := NewService(logger, repo, prod, nil, Config{Rail: "card"})
|
|
intent := &model.PaymentGatewayIntent{
|
|
PaymentIntentID: "pi-reject",
|
|
IdempotencyKey: "idem-reject",
|
|
QuoteRef: "quote-reject",
|
|
OutgoingLeg: "card",
|
|
RequestedMoney: &paymenttypes.Money{Amount: "3", Currency: "USD"},
|
|
}
|
|
svc.trackIntent("idem-reject", intent)
|
|
|
|
result := &model.ConfirmationResult{
|
|
RequestID: "idem-reject",
|
|
Status: model.ConfirmationStatusRejected,
|
|
RawReply: &model.TelegramMessage{ChatID: "1", MessageID: "4", Text: "no"},
|
|
}
|
|
if err := svc.onConfirmationResult(context.Background(), result); err != nil {
|
|
t.Fatalf("onConfirmationResult error: %v", err)
|
|
}
|
|
if repo.payments.executions["idem-reject"] != nil {
|
|
t.Fatalf("expected no execution record for rejection")
|
|
}
|
|
if repo.tg.records["idem-reject"] == nil {
|
|
t.Fatalf("expected raw reply to be stored for rejection")
|
|
}
|
|
}
|