Files
payments/internal/storage/payment_initiations_test.go

1571 lines
54 KiB
Go

package storage
import (
"context"
"encoding/json"
"fmt"
"math/big"
"testing"
"github.com/hanzo-fi/go-libs/v5/pkg/observe/log"
"github.com/hanzo-fi/go-libs/v5/pkg/query"
"github.com/hanzo-fi/go-libs/v5/pkg/storage/bun/paginate"
"github.com/hanzo-fi/go-libs/v5/pkg/types/pointer"
"github.com/hanzo-fi/go-libs/v5/pkg/types/time"
"github.com/hanzo-fi/payments/pkg/domain/models"
"github.com/hanzo-fi/payments/pkg/events"
"github.com/google/uuid"
"github.com/pkg/errors"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
var (
piID1 = models.PaymentInitiationID{
Reference: "test1",
ConnectorID: defaultConnector.ID,
}
piID2 = models.PaymentInitiationID{
Reference: "test2",
ConnectorID: defaultConnector.ID,
}
piID3 = models.PaymentInitiationID{
Reference: "test3",
ConnectorID: defaultConnector.ID,
}
)
func defaultPaymentInitiations() []models.PaymentInitiation {
defaultAccounts := defaultAccounts()
return []models.PaymentInitiation{
{
ID: piID1,
ConnectorID: defaultConnector.ID,
Reference: "test1",
CreatedAt: now.Add(-60 * time.Minute).UTC().Time,
ScheduledAt: now.Add(-60 * time.Minute).UTC().Time,
Description: "test1",
Type: models.PAYMENT_INITIATION_TYPE_PAYOUT,
DestinationAccountID: &defaultAccounts[0].ID,
Amount: big.NewInt(100),
Asset: "EUR/2",
},
{
ID: piID2,
ConnectorID: defaultConnector.ID,
Reference: "test2",
CreatedAt: now.Add(-30 * time.Minute).UTC().Time,
ScheduledAt: now.Add(-20 * time.Minute).UTC().Time,
Description: "test2",
Type: models.PAYMENT_INITIATION_TYPE_TRANSFER,
SourceAccountID: &defaultAccounts[0].ID,
DestinationAccountID: &defaultAccounts[1].ID,
Amount: big.NewInt(150),
Asset: "USD/2",
Metadata: map[string]string{
"foo": "bar",
},
},
{
ID: piID3,
ConnectorID: defaultConnector.ID,
Reference: "test3",
CreatedAt: now.Add(-55 * time.Minute).UTC().Time,
ScheduledAt: now.Add(-40 * time.Minute).UTC().Time,
Description: "test3",
Type: models.PAYMENT_INITIATION_TYPE_PAYOUT,
DestinationAccountID: &defaultAccounts[1].ID,
Amount: big.NewInt(200),
Asset: "EUR/2",
Metadata: map[string]string{
"foo2": "bar2",
},
},
}
}
func upsertPaymentInitiations(t *testing.T, ctx context.Context, storage Storage, paymentInitiations []models.PaymentInitiation) {
for _, pi := range paymentInitiations {
err := storage.PaymentInitiationsInsert(ctx, pi)
require.NoError(t, err)
}
}
func TestPaymentInitiationsInsert(t *testing.T) {
t.Parallel()
ctx := logging.TestingContext()
store := newStore(t)
defer store.Close()
upsertConnector(t, ctx, store, defaultConnector)
upsertAccounts(t, ctx, store, defaultAccounts())
upsertPaymentInitiations(t, ctx, store, defaultPaymentInitiations())
t.Run("upsert with unknown connector", func(t *testing.T) {
connector := models.ConnectorID{
Reference: uuid.New(),
Provider: "unknown",
}
p := defaultPaymentInitiations()[0]
p.ID.ConnectorID = connector
p.ConnectorID = connector
err := store.PaymentInitiationsInsert(ctx, p)
require.Error(t, err)
})
t.Run("attempt insert with same id", func(t *testing.T) {
defaultAccounts := defaultAccounts()
pi := models.PaymentInitiation{
ID: piID1,
ConnectorID: defaultConnector.ID,
Reference: "test_changed",
CreatedAt: now.Add(-30 * time.Minute).UTC().Time,
ScheduledAt: now.Add(-20 * time.Minute).UTC().Time,
Description: "test_changed",
Type: models.PAYMENT_INITIATION_TYPE_PAYOUT,
DestinationAccountID: &defaultAccounts[0].ID,
Amount: big.NewInt(100),
Asset: "DKK/2",
}
err := store.PaymentInitiationsInsert(ctx, pi)
require.Error(t, err)
assert.ErrorIs(t, err, ErrDuplicateKeyValue)
actual, err := store.PaymentInitiationsGet(ctx, piID1)
require.NoError(t, err)
comparePaymentInitiations(t, defaultPaymentInitiations()[0], *actual)
})
t.Run("outbox event created for payment initiation", func(t *testing.T) {
// Create a new payment initiation for this test
defaultAccounts := defaultAccounts()
newPI := models.PaymentInitiation{
ID: models.PaymentInitiationID{
Reference: "outbox-test-pi",
ConnectorID: defaultConnector.ID,
},
ConnectorID: defaultConnector.ID,
Reference: "outbox-test-pi",
CreatedAt: now.Add(-10 * time.Minute).UTC().Time,
ScheduledAt: now.Add(-5 * time.Minute).UTC().Time,
Description: "Test Payment Initiation",
Type: models.PAYMENT_INITIATION_TYPE_PAYOUT,
DestinationAccountID: &defaultAccounts[0].ID,
Amount: big.NewInt(1000),
Asset: "USD/2",
Metadata: map[string]string{
"test": "outbox",
},
}
expectedKey := newPI.IdempotencyKey()
require.NoError(t, store.PaymentInitiationsInsert(ctx, newPI))
// Verify outbox event was created
pendingEvents, err := store.OutboxEventsPollPending(ctx, 100)
require.NoError(t, err)
// Find our event
var ourEvent *models.OutboxEvent
for i := range pendingEvents {
if pendingEvents[i].EventType == events.EventTypeSavedPaymentInitiation &&
pendingEvents[i].EntityID == newPI.ID.String() &&
pendingEvents[i].ID.EventIdempotencyKey == expectedKey {
ourEvent = &pendingEvents[i]
break
}
}
require.NotNil(t, ourEvent, "expected outbox event for payment initiation saved")
// Verify event details
assert.Equal(t, events.EventTypeSavedPaymentInitiation, ourEvent.EventType)
assert.Equal(t, models.OUTBOX_STATUS_PENDING, ourEvent.Status)
assert.Equal(t, newPI.ID.String(), ourEvent.EntityID)
assert.Equal(t, newPI.ConnectorID, *ourEvent.ConnectorID)
assert.Equal(t, 0, ourEvent.RetryCount)
assert.Equal(t, expectedKey, ourEvent.ID.EventIdempotencyKey)
// Verify payload
var payload map[string]interface{}
err = json.Unmarshal(ourEvent.Payload, &payload)
require.NoError(t, err)
assert.Equal(t, newPI.ID.String(), payload["id"])
assert.Equal(t, newPI.ConnectorID.String(), payload["connectorID"])
assert.Equal(t, newPI.Reference, payload["reference"])
assert.Equal(t, newPI.Amount.String(), payload["amount"])
assert.Equal(t, newPI.Asset, payload["asset"])
})
}
func TestPaymentInitiationsUpdateMetadata(t *testing.T) {
t.Parallel()
ctx := logging.TestingContext()
store := newStore(t)
defer store.Close()
upsertConnector(t, ctx, store, defaultConnector)
upsertAccounts(t, ctx, store, defaultAccounts())
upsertPaymentInitiations(t, ctx, store, defaultPaymentInitiations())
t.Run("update metadata of unknown payment initiation", func(t *testing.T) {
require.Error(t, store.PaymentInitiationsUpdateMetadata(ctx, models.PaymentInitiationID{
Reference: "unknown",
ConnectorID: defaultConnector.ID,
}, map[string]string{}))
})
t.Run("update existing metadata", func(t *testing.T) {
metadata := map[string]string{
"foo": "changed",
}
require.NoError(t, store.PaymentInitiationsUpdateMetadata(ctx, piID2, metadata))
actual, err := store.PaymentInitiationsGet(ctx, piID2)
require.NoError(t, err)
require.Equal(t, len(metadata), len(actual.Metadata))
for k, v := range metadata {
_, ok := actual.Metadata[k]
require.True(t, ok)
require.Equal(t, v, actual.Metadata[k])
}
})
t.Run("add new metadata", func(t *testing.T) {
metadata := map[string]string{
"key2": "value2",
"key3": "value3",
}
require.NoError(t, store.PaymentInitiationsUpdateMetadata(ctx, piID1, metadata))
actual, err := store.PaymentInitiationsGet(ctx, piID1)
require.NoError(t, err)
require.Equal(t, len(metadata), len(actual.Metadata))
for k, v := range metadata {
_, ok := actual.Metadata[k]
require.True(t, ok)
require.Equal(t, v, actual.Metadata[k])
}
})
}
func TestPaymentInitiationsGet(t *testing.T) {
t.Parallel()
ctx := logging.TestingContext()
store := newStore(t)
defer store.Close()
upsertConnector(t, ctx, store, defaultConnector)
upsertAccounts(t, ctx, store, defaultAccounts())
upsertPaymentInitiations(t, ctx, store, defaultPaymentInitiations())
t.Run("get unknown payment initiation", func(t *testing.T) {
_, err := store.PaymentInitiationsGet(ctx, models.PaymentInitiationID{
Reference: "unknown",
ConnectorID: defaultConnector.ID,
})
require.Error(t, err)
require.ErrorIs(t, err, ErrNotFound)
})
t.Run("get existing payment initiation", func(t *testing.T) {
for _, pi := range defaultPaymentInitiations() {
actual, err := store.PaymentInitiationsGet(ctx, pi.ID)
require.NoError(t, err)
comparePaymentInitiations(t, pi, *actual)
}
})
}
func TestPaymentInitiationsDelete(t *testing.T) {
t.Parallel()
ctx := logging.TestingContext()
store := newStore(t)
defer store.Close()
upsertConnector(t, ctx, store, defaultConnector)
upsertAccounts(t, ctx, store, defaultAccounts())
upsertPaymentInitiations(t, ctx, store, defaultPaymentInitiations())
t.Run("delete unknown payment initiation", func(t *testing.T) {
require.NoError(t, store.PaymentInitiationsDelete(ctx, models.PaymentInitiationID{
Reference: "unknown",
ConnectorID: defaultConnector.ID,
}))
})
t.Run("delete existing payment initiation", func(t *testing.T) {
for _, pi := range defaultPaymentInitiations() {
require.NoError(t, store.PaymentInitiationsDelete(ctx, pi.ID))
_, err := store.PaymentInitiationsGet(ctx, pi.ID)
require.Error(t, err)
require.ErrorIs(t, err, ErrNotFound)
}
})
}
func TestPaymentInitiationsDeleteFromConnectorID(t *testing.T) {
t.Parallel()
ctx := logging.TestingContext()
store := newStore(t)
defer store.Close()
upsertConnector(t, ctx, store, defaultConnector)
upsertAccounts(t, ctx, store, defaultAccounts())
upsertPaymentInitiations(t, ctx, store, defaultPaymentInitiations())
t.Run("delete from unknown connector", func(t *testing.T) {
require.NoError(t, store.PaymentInitiationsDeleteFromConnectorID(ctx, models.ConnectorID{
Reference: uuid.New(),
Provider: "unknown",
}))
for _, pi := range defaultPaymentInitiations() {
actual, err := store.PaymentInitiationsGet(ctx, pi.ID)
require.NoError(t, err)
comparePaymentInitiations(t, pi, *actual)
}
})
t.Run("delete from existing connector", func(t *testing.T) {
require.NoError(t, store.PaymentInitiationsDeleteFromConnectorID(ctx, defaultConnector.ID))
for _, pi := range defaultPaymentInitiations() {
_, err := store.PaymentInitiationsGet(ctx, pi.ID)
require.Error(t, err)
require.ErrorIs(t, err, ErrNotFound)
}
})
}
func TestPaymentInitiationsList(t *testing.T) {
t.Parallel()
ctx := logging.TestingContext()
store := newStore(t)
defer store.Close()
upsertConnector(t, ctx, store, defaultConnector)
upsertAccounts(t, ctx, store, defaultAccounts())
upsertPaymentInitiations(t, ctx, store, defaultPaymentInitiations())
upsertPaymentInitiationAdjustments(t, ctx, store, defaultPaymentInitiationAdjustments())
t.Run("wrong query builder operator when listing by reference", func(t *testing.T) {
q := NewListPaymentInitiationsQuery(
paginate.NewPaginatedQueryOptions(PaymentInitiationQuery{}).
WithPageSize(15).
WithQueryBuilder(query.Lt("reference", "test1")),
)
cursor, err := store.PaymentInitiationsList(ctx, q)
require.Error(t, err)
require.Nil(t, cursor)
assert.True(t, errors.Is(err, ErrValidation))
assert.Regexp(t, "reference", err.Error())
})
t.Run("list payment intitiations by reference", func(t *testing.T) {
q := NewListPaymentInitiationsQuery(
paginate.NewPaginatedQueryOptions(PaymentInitiationQuery{}).
WithPageSize(15).
WithQueryBuilder(query.Match("reference", "test1")),
)
cursor, err := store.PaymentInitiationsList(ctx, q)
require.NoError(t, err)
require.Len(t, cursor.Data, 1)
require.False(t, cursor.HasMore)
comparePaymentInitiations(t, defaultPaymentInitiations()[0], cursor.Data[0])
})
t.Run("list payment initiations by unknown reference", func(t *testing.T) {
q := NewListPaymentInitiationsQuery(
paginate.NewPaginatedQueryOptions(PaymentInitiationQuery{}).
WithPageSize(15).
WithQueryBuilder(query.Match("reference", "unknown")),
)
cursor, err := store.PaymentInitiationsList(ctx, q)
require.NoError(t, err)
require.Len(t, cursor.Data, 0)
require.False(t, cursor.HasMore)
})
t.Run("list payment intitiations by id", func(t *testing.T) {
q := NewListPaymentInitiationsQuery(
paginate.NewPaginatedQueryOptions(PaymentInitiationQuery{}).
WithPageSize(15).
WithQueryBuilder(query.Match("id", defaultPaymentInitiations()[0].ID.String())),
)
cursor, err := store.PaymentInitiationsList(ctx, q)
require.NoError(t, err)
require.Len(t, cursor.Data, 1)
require.False(t, cursor.HasMore)
comparePaymentInitiations(t, defaultPaymentInitiations()[0], cursor.Data[0])
})
t.Run("list payment initiations by unknown id", func(t *testing.T) {
q := NewListPaymentInitiationsQuery(
paginate.NewPaginatedQueryOptions(PaymentInitiationQuery{}).
WithPageSize(15).
WithQueryBuilder(query.Match("id", "unknown")),
)
cursor, err := store.PaymentInitiationsList(ctx, q)
require.NoError(t, err)
require.Len(t, cursor.Data, 0)
require.False(t, cursor.HasMore)
})
t.Run("list payment initiations by connector_id", func(t *testing.T) {
q := NewListPaymentInitiationsQuery(
paginate.NewPaginatedQueryOptions(PaymentInitiationQuery{}).
WithPageSize(15).
WithQueryBuilder(query.Match("connector_id", defaultConnector.ID.String())),
)
cursor, err := store.PaymentInitiationsList(ctx, q)
require.NoError(t, err)
require.Len(t, cursor.Data, 3)
require.False(t, cursor.HasMore)
comparePaymentInitiations(t, defaultPaymentInitiations()[1], cursor.Data[0])
comparePaymentInitiations(t, defaultPaymentInitiations()[2], cursor.Data[1])
comparePaymentInitiations(t, defaultPaymentInitiations()[0], cursor.Data[2])
})
t.Run("list payment initiations by unknown connector_id", func(t *testing.T) {
q := NewListPaymentInitiationsQuery(
paginate.NewPaginatedQueryOptions(PaymentInitiationQuery{}).
WithPageSize(15).
WithQueryBuilder(query.Match("connector_id", "unknown")),
)
cursor, err := store.PaymentInitiationsList(ctx, q)
require.NoError(t, err)
require.Len(t, cursor.Data, 0)
require.False(t, cursor.HasMore)
})
t.Run("list payment initiations by type", func(t *testing.T) {
q := NewListPaymentInitiationsQuery(
paginate.NewPaginatedQueryOptions(PaymentInitiationQuery{}).
WithPageSize(15).
WithQueryBuilder(query.Match("type", "PAYOUT")),
)
cursor, err := store.PaymentInitiationsList(ctx, q)
require.NoError(t, err)
require.Len(t, cursor.Data, 2)
require.False(t, cursor.HasMore)
comparePaymentInitiations(t, defaultPaymentInitiations()[2], cursor.Data[0])
comparePaymentInitiations(t, defaultPaymentInitiations()[0], cursor.Data[1])
})
t.Run("list payment initiations by type 2", func(t *testing.T) {
q := NewListPaymentInitiationsQuery(
paginate.NewPaginatedQueryOptions(PaymentInitiationQuery{}).
WithPageSize(15).
WithQueryBuilder(query.Match("type", "TRANSFER")),
)
cursor, err := store.PaymentInitiationsList(ctx, q)
require.NoError(t, err)
require.Len(t, cursor.Data, 1)
require.False(t, cursor.HasMore)
comparePaymentInitiations(t, defaultPaymentInitiations()[1], cursor.Data[0])
})
t.Run("list payment initiations by unknown type", func(t *testing.T) {
q := NewListPaymentInitiationsQuery(
paginate.NewPaginatedQueryOptions(PaymentInitiationQuery{}).
WithPageSize(15).
WithQueryBuilder(query.Match("type", "UNKNOWN")),
)
cursor, err := store.PaymentInitiationsList(ctx, q)
require.NoError(t, err)
require.Len(t, cursor.Data, 0)
require.False(t, cursor.HasMore)
})
t.Run("list payment initiations by status multiple adjustments", func(t *testing.T) {
q := NewListPaymentInitiationsQuery(
paginate.NewPaginatedQueryOptions(PaymentInitiationQuery{}).
WithPageSize(15).
WithQueryBuilder(query.Match("status", models.PAYMENT_INITIATION_ADJUSTMENT_STATUS_FAILED.String())),
)
cursor, err := store.PaymentInitiationsList(ctx, q)
require.NoError(t, err)
require.Len(t, cursor.Data, 1)
require.False(t, cursor.HasMore)
comparePaymentInitiations(t, defaultPaymentInitiations()[0], cursor.Data[0])
})
t.Run("list payment initiations by status single adjustment", func(t *testing.T) {
q := NewListPaymentInitiationsQuery(
paginate.NewPaginatedQueryOptions(PaymentInitiationQuery{}).
WithPageSize(15).
WithQueryBuilder(query.Match("status", models.PAYMENT_INITIATION_ADJUSTMENT_STATUS_PROCESSING.String())),
)
cursor, err := store.PaymentInitiationsList(ctx, q)
require.NoError(t, err)
require.Len(t, cursor.Data, 1)
require.False(t, cursor.HasMore)
comparePaymentInitiations(t, defaultPaymentInitiations()[1], cursor.Data[0])
})
t.Run("list payment initiations by unknown status", func(t *testing.T) {
q := NewListPaymentInitiationsQuery(
paginate.NewPaginatedQueryOptions(PaymentInitiationQuery{}).
WithPageSize(15).
WithQueryBuilder(query.Match("status", "UNKNOWN")),
)
cursor, err := store.PaymentInitiationsList(ctx, q)
require.NoError(t, err)
require.Len(t, cursor.Data, 0)
require.False(t, cursor.HasMore)
})
t.Run("list payment initiations by asset", func(t *testing.T) {
q := NewListPaymentInitiationsQuery(
paginate.NewPaginatedQueryOptions(PaymentInitiationQuery{}).
WithPageSize(15).
WithQueryBuilder(query.Match("asset", "EUR/2")),
)
cursor, err := store.PaymentInitiationsList(ctx, q)
require.NoError(t, err)
require.Len(t, cursor.Data, 2)
require.False(t, cursor.HasMore)
comparePaymentInitiations(t, defaultPaymentInitiations()[2], cursor.Data[0])
comparePaymentInitiations(t, defaultPaymentInitiations()[0], cursor.Data[1])
})
t.Run("list payment initiations by asset 2", func(t *testing.T) {
q := NewListPaymentInitiationsQuery(
paginate.NewPaginatedQueryOptions(PaymentInitiationQuery{}).
WithPageSize(15).
WithQueryBuilder(query.Match("asset", "USD/2")),
)
cursor, err := store.PaymentInitiationsList(ctx, q)
require.NoError(t, err)
require.Len(t, cursor.Data, 1)
require.False(t, cursor.HasMore)
comparePaymentInitiations(t, defaultPaymentInitiations()[1], cursor.Data[0])
})
t.Run("list payment initiations by unknown asset", func(t *testing.T) {
q := NewListPaymentInitiationsQuery(
paginate.NewPaginatedQueryOptions(PaymentInitiationQuery{}).
WithPageSize(15).
WithQueryBuilder(query.Match("asset", "unknown")),
)
cursor, err := store.PaymentInitiationsList(ctx, q)
require.NoError(t, err)
require.Len(t, cursor.Data, 0)
require.False(t, cursor.HasMore)
})
t.Run("list payment initiations by source account id", func(t *testing.T) {
q := NewListPaymentInitiationsQuery(
paginate.NewPaginatedQueryOptions(PaymentInitiationQuery{}).
WithPageSize(15).
WithQueryBuilder(query.Match("source_account_id", defaultAccounts()[0].ID.String())),
)
cursor, err := store.PaymentInitiationsList(ctx, q)
require.NoError(t, err)
require.Len(t, cursor.Data, 1)
require.False(t, cursor.HasMore)
comparePaymentInitiations(t, defaultPaymentInitiations()[1], cursor.Data[0])
})
t.Run("list payment initiations by unknown source account id", func(t *testing.T) {
q := NewListPaymentInitiationsQuery(
paginate.NewPaginatedQueryOptions(PaymentInitiationQuery{}).
WithPageSize(15).
WithQueryBuilder(query.Match("source_account_id", "unknown")),
)
cursor, err := store.PaymentInitiationsList(ctx, q)
require.NoError(t, err)
require.Len(t, cursor.Data, 0)
require.False(t, cursor.HasMore)
})
t.Run("list payment initiations by destination account id", func(t *testing.T) {
q := NewListPaymentInitiationsQuery(
paginate.NewPaginatedQueryOptions(PaymentInitiationQuery{}).
WithPageSize(15).
WithQueryBuilder(query.Match("destination_account_id", defaultAccounts()[1].ID.String())),
)
cursor, err := store.PaymentInitiationsList(ctx, q)
require.NoError(t, err)
require.Len(t, cursor.Data, 2)
require.False(t, cursor.HasMore)
comparePaymentInitiations(t, defaultPaymentInitiations()[1], cursor.Data[0])
comparePaymentInitiations(t, defaultPaymentInitiations()[2], cursor.Data[1])
})
t.Run("list payment initiations by unknown destination account id", func(t *testing.T) {
q := NewListPaymentInitiationsQuery(
paginate.NewPaginatedQueryOptions(PaymentInitiationQuery{}).
WithPageSize(15).
WithQueryBuilder(query.Match("destination_account_id", "unknown")),
)
cursor, err := store.PaymentInitiationsList(ctx, q)
require.NoError(t, err)
require.Len(t, cursor.Data, 0)
require.False(t, cursor.HasMore)
})
t.Run("list payment initiations by amount", func(t *testing.T) {
q := NewListPaymentInitiationsQuery(
paginate.NewPaginatedQueryOptions(PaymentInitiationQuery{}).
WithPageSize(15).
WithQueryBuilder(query.Match("amount", 200)),
)
cursor, err := store.PaymentInitiationsList(ctx, q)
require.NoError(t, err)
require.Len(t, cursor.Data, 1)
require.False(t, cursor.HasMore)
comparePaymentInitiations(t, defaultPaymentInitiations()[2], cursor.Data[0])
})
t.Run("list payment initiations by unknown amount", func(t *testing.T) {
q := NewListPaymentInitiationsQuery(
paginate.NewPaginatedQueryOptions(PaymentInitiationQuery{}).
WithPageSize(15).
WithQueryBuilder(query.Match("amount", 0)),
)
cursor, err := store.PaymentInitiationsList(ctx, q)
require.NoError(t, err)
require.Len(t, cursor.Data, 0)
require.False(t, cursor.HasMore)
})
t.Run("wrong query builder operator when listing by metadata", func(t *testing.T) {
q := NewListPaymentInitiationsQuery(
paginate.NewPaginatedQueryOptions(PaymentInitiationQuery{}).
WithPageSize(15).
WithQueryBuilder(query.Lt("metadata[foo]", "bar")),
)
cursor, err := store.PaymentInitiationsList(ctx, q)
require.Error(t, err)
require.Nil(t, cursor)
})
t.Run("list payment initiations by metadata", func(t *testing.T) {
q := NewListPaymentInitiationsQuery(
paginate.NewPaginatedQueryOptions(PaymentInitiationQuery{}).
WithPageSize(15).
WithQueryBuilder(query.Match("metadata[foo]", "bar")),
)
cursor, err := store.PaymentInitiationsList(ctx, q)
require.NoError(t, err)
require.Len(t, cursor.Data, 1)
require.False(t, cursor.HasMore)
comparePaymentInitiations(t, defaultPaymentInitiations()[1], cursor.Data[0])
})
t.Run("list payment initiations by unknown metadata", func(t *testing.T) {
q := NewListPaymentInitiationsQuery(
paginate.NewPaginatedQueryOptions(PaymentInitiationQuery{}).
WithPageSize(15).
WithQueryBuilder(query.Match("metadata[foo]", "unknown")),
)
cursor, err := store.PaymentInitiationsList(ctx, q)
require.NoError(t, err)
require.Len(t, cursor.Data, 0)
require.False(t, cursor.HasMore)
})
t.Run("list payment initiations by unknown metadata 2", func(t *testing.T) {
q := NewListPaymentInitiationsQuery(
paginate.NewPaginatedQueryOptions(PaymentInitiationQuery{}).
WithPageSize(15).
WithQueryBuilder(query.Match("metadata[unknown]", "bar")),
)
cursor, err := store.PaymentInitiationsList(ctx, q)
require.NoError(t, err)
require.Len(t, cursor.Data, 0)
require.False(t, cursor.HasMore)
})
t.Run("unknown query builder key when listing", func(t *testing.T) {
q := NewListPaymentInitiationsQuery(
paginate.NewPaginatedQueryOptions(PaymentInitiationQuery{}).
WithPageSize(15).
WithQueryBuilder(query.Match("unknown", "test1")),
)
cursor, err := store.PaymentInitiationsList(ctx, q)
require.Error(t, err)
require.Nil(t, cursor)
})
t.Run("list payment initiations test cursor", func(t *testing.T) {
q := NewListPaymentInitiationsQuery(
paginate.NewPaginatedQueryOptions(PaymentInitiationQuery{}).
WithPageSize(1),
)
cursor, err := store.PaymentInitiationsList(ctx, q)
require.NoError(t, err)
require.Len(t, cursor.Data, 1)
require.True(t, cursor.HasMore)
require.Empty(t, cursor.Previous)
require.NotEmpty(t, cursor.Next)
comparePaymentInitiations(t, defaultPaymentInitiations()[1], cursor.Data[0])
err = paginate.UnmarshalCursor(cursor.Next, &q)
require.NoError(t, err)
cursor, err = store.PaymentInitiationsList(ctx, q)
require.NoError(t, err)
require.Len(t, cursor.Data, 1)
require.True(t, cursor.HasMore)
require.NotEmpty(t, cursor.Previous)
require.NotEmpty(t, cursor.Next)
comparePaymentInitiations(t, defaultPaymentInitiations()[2], cursor.Data[0])
err = paginate.UnmarshalCursor(cursor.Next, &q)
require.NoError(t, err)
cursor, err = store.PaymentInitiationsList(ctx, q)
require.NoError(t, err)
require.Len(t, cursor.Data, 1)
require.False(t, cursor.HasMore)
require.NotEmpty(t, cursor.Previous)
require.Empty(t, cursor.Next)
comparePaymentInitiations(t, defaultPaymentInitiations()[0], cursor.Data[0])
err = paginate.UnmarshalCursor(cursor.Previous, &q)
require.NoError(t, err)
cursor, err = store.PaymentInitiationsList(ctx, q)
require.NoError(t, err)
require.Len(t, cursor.Data, 1)
require.True(t, cursor.HasMore)
require.NotEmpty(t, cursor.Previous)
require.NotEmpty(t, cursor.Next)
comparePaymentInitiations(t, defaultPaymentInitiations()[2], cursor.Data[0])
err = paginate.UnmarshalCursor(cursor.Previous, &q)
require.NoError(t, err)
cursor, err = store.PaymentInitiationsList(ctx, q)
require.NoError(t, err)
require.Len(t, cursor.Data, 1)
require.True(t, cursor.HasMore)
require.Empty(t, cursor.Previous)
require.NotEmpty(t, cursor.Next)
comparePaymentInitiations(t, defaultPaymentInitiations()[1], cursor.Data[0])
})
}
func upsertPaymentInitiationRelatedPayments(t *testing.T, ctx context.Context, storage Storage) {
payments := defaultPayments()
require.NoError(t, storage.PaymentInitiationRelatedPaymentsUpsert(ctx, piID1, payments[0].ID, now.Add(-10*time.Minute).UTC().Time))
require.NoError(t, storage.PaymentInitiationRelatedPaymentsUpsert(ctx, piID1, payments[1].ID, now.Add(-5*time.Minute).UTC().Time))
require.NoError(t, storage.PaymentInitiationRelatedPaymentsUpsert(ctx, piID1, payments[2].ID, now.Add(-7*time.Minute).UTC().Time))
require.NoError(t, storage.PaymentInitiationRelatedPaymentsUpsert(ctx, piID2, payments[0].ID, now.Add(-7*time.Minute).UTC().Time))
}
func TestPaymentInitiationsRelatedPaymentUpsert(t *testing.T) {
t.Parallel()
ctx := logging.TestingContext()
store := newStore(t)
defer store.Close()
upsertConnector(t, ctx, store, defaultConnector)
upsertAccounts(t, ctx, store, defaultAccounts())
upsertPayments(t, ctx, store, defaultPayments())
upsertPaymentInitiations(t, ctx, store, defaultPaymentInitiations())
upsertPaymentInitiationRelatedPayments(t, ctx, store)
t.Run("same id insert", func(t *testing.T) {
payments := defaultPayments()
require.NoError(t, store.PaymentInitiationRelatedPaymentsUpsert(ctx, piID1, payments[0].ID, now.Add(-10*time.Minute).UTC().Time))
cursor, err := store.PaymentInitiationRelatedPaymentsList(
ctx,
piID1,
NewListPaymentInitiationRelatedPaymentsQuery(
paginate.NewPaginatedQueryOptions(PaymentInitiationRelatedPaymentsQuery{}).
WithPageSize(15),
),
)
require.NoError(t, err)
require.Len(t, cursor.Data, 3)
require.False(t, cursor.HasMore)
comparePayments(t, payments[1], cursor.Data[0])
comparePayments(t, payments[2], cursor.Data[1])
comparePayments(t, payments[0], cursor.Data[2])
})
t.Run("outbox event created for new related payment", func(t *testing.T) {
// Clean up outbox events before test
defer cleanupOutboxHelper(ctx, store)()
// Create a new related payment for this test
payments := defaultPayments()
newPI := models.PaymentInitiation{
ID: models.PaymentInitiationID{
Reference: "outbox-test-pi-related",
ConnectorID: defaultConnector.ID,
},
ConnectorID: defaultConnector.ID,
Reference: "outbox-test-pi-related",
CreatedAt: now.Add(-10 * time.Minute).UTC().Time,
ScheduledAt: now.Add(-5 * time.Minute).UTC().Time,
Type: models.PAYMENT_INITIATION_TYPE_PAYOUT,
Amount: big.NewInt(2000),
Asset: "EUR/2",
}
require.NoError(t, store.PaymentInitiationsInsert(ctx, newPI))
relatedPayment := models.PaymentInitiationRelatedPayments{
PaymentInitiationID: newPI.ID,
PaymentID: payments[0].ID,
}
expectedKey := relatedPayment.IdempotencyKey()
require.NoError(t, store.PaymentInitiationRelatedPaymentsUpsert(ctx, newPI.ID, payments[0].ID, now.Add(-1*time.Minute).UTC().Time))
// Verify outbox event was created
pendingEvents, err := store.OutboxEventsPollPending(ctx, 100)
require.NoError(t, err)
// Find our event
var ourEvent *models.OutboxEvent
for i := range pendingEvents {
if pendingEvents[i].EventType == events.EventTypeSavedPaymentInitiationRelatedPayment &&
pendingEvents[i].EntityID == fmt.Sprintf("%s:%s", newPI.ID.String(), payments[0].ID.String()) &&
pendingEvents[i].ID.EventIdempotencyKey == expectedKey {
ourEvent = &pendingEvents[i]
break
}
}
require.NotNil(t, ourEvent, "expected outbox event for payment initiation related payment saved")
// Verify event details
assert.Equal(t, events.EventTypeSavedPaymentInitiationRelatedPayment, ourEvent.EventType)
assert.Equal(t, models.OUTBOX_STATUS_PENDING, ourEvent.Status)
assert.Equal(t, fmt.Sprintf("%s:%s", newPI.ID.String(), payments[0].ID.String()), ourEvent.EntityID)
assert.Equal(t, newPI.ConnectorID, *ourEvent.ConnectorID)
assert.Equal(t, 0, ourEvent.RetryCount)
assert.Equal(t, expectedKey, ourEvent.ID.EventIdempotencyKey)
// Verify payload
var payload map[string]interface{}
err = json.Unmarshal(ourEvent.Payload, &payload)
require.NoError(t, err)
assert.Equal(t, newPI.ID.String(), payload["paymentInitiationID"])
assert.Equal(t, payments[0].ID.String(), payload["paymentID"])
})
t.Run("no outbox event for existing related payment", func(t *testing.T) {
// Clean up outbox events before test
defer cleanupOutboxHelper(ctx, store)()
// Count events before
eventsBefore, err := store.OutboxEventsPollPending(ctx, 1000)
require.NoError(t, err)
countBefore := len(eventsBefore)
// Insert existing related payment (should not create event)
payments := defaultPayments()
require.NoError(t, store.PaymentInitiationRelatedPaymentsUpsert(ctx, piID1, payments[0].ID, now.Add(-10*time.Minute).UTC().Time))
// Verify no new outbox event was created
eventsAfter, err := store.OutboxEventsPollPending(ctx, 1000)
require.NoError(t, err)
countAfter := len(eventsAfter)
// Should have same number of events (no new related payment saved event)
assert.Equal(t, countBefore, countAfter, "updating existing related payment should not create saved event")
})
t.Run("unknown payment initiation", func(t *testing.T) {
payments := defaultPayments()
require.Error(t, store.PaymentInitiationRelatedPaymentsUpsert(
ctx,
models.PaymentInitiationID{},
payments[0].ID, now.Add(-10*time.Minute).UTC().Time),
)
})
}
func TestPaymentInitiationIDsFromPaymentID(t *testing.T) {
t.Parallel()
ctx := logging.TestingContext()
store := newStore(t)
defer store.Close()
upsertConnector(t, ctx, store, defaultConnector)
upsertAccounts(t, ctx, store, defaultAccounts())
upsertPayments(t, ctx, store, defaultPayments())
upsertPaymentInitiations(t, ctx, store, defaultPaymentInitiations())
upsertPaymentInitiationRelatedPayments(t, ctx, store)
t.Run("unknown payment id", func(t *testing.T) {
ids, err := store.PaymentInitiationIDsListFromPaymentID(ctx, models.PaymentID{})
require.NoError(t, err)
require.Len(t, ids, 0)
})
t.Run("known payment id", func(t *testing.T) {
ids, err := store.PaymentInitiationIDsListFromPaymentID(ctx, defaultPayments()[0].ID)
require.NoError(t, err)
require.Len(t, ids, 2)
require.Contains(t, ids, piID1)
require.Contains(t, ids, piID2)
})
}
func TestPaymentInitiationRelatedPaymentsList(t *testing.T) {
t.Parallel()
ctx := logging.TestingContext()
store := newStore(t)
defer store.Close()
upsertConnector(t, ctx, store, defaultConnector)
upsertAccounts(t, ctx, store, defaultAccounts())
upsertPayments(t, ctx, store, defaultPayments())
upsertPaymentInitiations(t, ctx, store, defaultPaymentInitiations())
upsertPaymentInitiationRelatedPayments(t, ctx, store)
t.Run("list related payments by unknown payment initiation", func(t *testing.T) {
cursor, err := store.PaymentInitiationRelatedPaymentsList(
ctx,
models.PaymentInitiationID{},
NewListPaymentInitiationRelatedPaymentsQuery(
paginate.NewPaginatedQueryOptions(PaymentInitiationRelatedPaymentsQuery{}).
WithPageSize(15),
),
)
require.NoError(t, err)
require.Len(t, cursor.Data, 0)
require.False(t, cursor.HasMore)
})
t.Run("list related payments by payment initiation", func(t *testing.T) {
q := NewListPaymentInitiationRelatedPaymentsQuery(
paginate.NewPaginatedQueryOptions(PaymentInitiationRelatedPaymentsQuery{}).
WithPageSize(1),
)
payments := defaultPayments()
cursor, err := store.PaymentInitiationRelatedPaymentsList(ctx, piID1, q)
require.NoError(t, err)
require.Len(t, cursor.Data, 1)
require.True(t, cursor.HasMore)
require.Empty(t, cursor.Previous)
require.NotEmpty(t, cursor.Next)
comparePayments(t, payments[1], cursor.Data[0])
err = paginate.UnmarshalCursor(cursor.Next, &q)
require.NoError(t, err)
cursor, err = store.PaymentInitiationRelatedPaymentsList(ctx, piID1, q)
require.NoError(t, err)
require.Len(t, cursor.Data, 1)
require.True(t, cursor.HasMore)
require.NotEmpty(t, cursor.Previous)
require.NotEmpty(t, cursor.Next)
comparePayments(t, payments[2], cursor.Data[0])
err = paginate.UnmarshalCursor(cursor.Next, &q)
require.NoError(t, err)
cursor, err = store.PaymentInitiationRelatedPaymentsList(ctx, piID1, q)
require.NoError(t, err)
require.Len(t, cursor.Data, 1)
require.False(t, cursor.HasMore)
require.NotEmpty(t, cursor.Previous)
require.Empty(t, cursor.Next)
comparePayments(t, payments[0], cursor.Data[0])
err = paginate.UnmarshalCursor(cursor.Previous, &q)
require.NoError(t, err)
cursor, err = store.PaymentInitiationRelatedPaymentsList(ctx, piID1, q)
require.NoError(t, err)
require.Len(t, cursor.Data, 1)
require.True(t, cursor.HasMore)
require.NotEmpty(t, cursor.Previous)
require.NotEmpty(t, cursor.Next)
comparePayments(t, payments[2], cursor.Data[0])
err = paginate.UnmarshalCursor(cursor.Previous, &q)
require.NoError(t, err)
cursor, err = store.PaymentInitiationRelatedPaymentsList(ctx, piID1, q)
require.NoError(t, err)
require.Len(t, cursor.Data, 1)
require.True(t, cursor.HasMore)
require.Empty(t, cursor.Previous)
require.NotEmpty(t, cursor.Next)
comparePayments(t, payments[1], cursor.Data[0])
})
}
var (
piAdjID1 = models.PaymentInitiationAdjustmentID{
PaymentInitiationID: defaultPaymentInitiations()[0].ID,
CreatedAt: now.Add(-10 * time.Minute).UTC().Time,
Status: models.PAYMENT_INITIATION_ADJUSTMENT_STATUS_WAITING_FOR_VALIDATION,
}
piAdjID2 = models.PaymentInitiationAdjustmentID{
PaymentInitiationID: defaultPaymentInitiations()[0].ID,
CreatedAt: now.Add(-5 * time.Minute).UTC().Time,
Status: models.PAYMENT_INITIATION_ADJUSTMENT_STATUS_FAILED,
}
piAdjID3 = models.PaymentInitiationAdjustmentID{
PaymentInitiationID: defaultPaymentInitiations()[1].ID,
CreatedAt: now.Add(-7 * time.Minute).UTC().Time,
Status: models.PAYMENT_INITIATION_ADJUSTMENT_STATUS_PROCESSING,
}
)
func defaultPaymentInitiationAdjustments() []models.PaymentInitiationAdjustment {
return []models.PaymentInitiationAdjustment{
{
ID: piAdjID1,
CreatedAt: now.Add(-10 * time.Minute).UTC().Time,
Status: models.PAYMENT_INITIATION_ADJUSTMENT_STATUS_WAITING_FOR_VALIDATION,
Amount: big.NewInt(100),
Asset: pointer.For("EUR/2"),
Metadata: map[string]string{
"foo": "bar",
},
},
{
ID: piAdjID2,
CreatedAt: now.Add(-5 * time.Minute).UTC().Time,
Status: models.PAYMENT_INITIATION_ADJUSTMENT_STATUS_FAILED,
Error: errors.New("test"),
Amount: big.NewInt(200),
Asset: pointer.For("USD/2"),
Metadata: map[string]string{
"foo2": "bar2",
},
},
{
ID: piAdjID3,
CreatedAt: now.Add(-7 * time.Minute).UTC().Time,
Status: models.PAYMENT_INITIATION_ADJUSTMENT_STATUS_PROCESSING,
Amount: big.NewInt(300),
Asset: pointer.For("DKK/2"),
Metadata: map[string]string{
"foo3": "bar3",
},
},
}
}
func upsertPaymentInitiationAdjustments(t *testing.T, ctx context.Context, storage Storage, adjustments []models.PaymentInitiationAdjustment) {
for _, adj := range adjustments {
require.NoError(t, storage.PaymentInitiationAdjustmentsUpsert(ctx, adj))
}
}
func TestPaymentInitiationAdjustmentsUpsert(t *testing.T) {
t.Parallel()
ctx := logging.TestingContext()
store := newStore(t)
defer store.Close()
upsertConnector(t, ctx, store, defaultConnector)
upsertAccounts(t, ctx, store, defaultAccounts())
upsertPayments(t, ctx, store, defaultPayments())
upsertPaymentInitiations(t, ctx, store, defaultPaymentInitiations())
upsertPaymentInitiationAdjustments(t, ctx, store, defaultPaymentInitiationAdjustments())
t.Run("upsert with unknown payment initiation", func(t *testing.T) {
p := models.PaymentInitiationAdjustment{
ID: models.PaymentInitiationAdjustmentID{},
CreatedAt: now.Add(-10 * time.Minute).UTC().Time,
Status: models.PAYMENT_INITIATION_ADJUSTMENT_STATUS_WAITING_FOR_VALIDATION,
Metadata: map[string]string{
"foo": "bar",
},
}
require.Error(t, store.PaymentInitiationAdjustmentsUpsert(ctx, p))
})
t.Run("upsert with same id", func(t *testing.T) {
p := models.PaymentInitiationAdjustment{
ID: piAdjID1,
CreatedAt: now.Add(-30 * time.Minute).UTC().Time,
Status: models.PAYMENT_INITIATION_ADJUSTMENT_STATUS_PROCESSED,
Metadata: map[string]string{
"foo": "changed",
},
}
require.NoError(t, store.PaymentInitiationAdjustmentsUpsert(ctx, p))
for _, pa := range defaultPaymentInitiationAdjustments() {
actual, err := store.PaymentInitiationAdjustmentsGet(ctx, pa.ID)
require.NoError(t, err)
comparePaymentInitiationAdjustments(t, pa, *actual)
}
})
t.Run("outbox event created for new adjustment", func(t *testing.T) {
// Clean up outbox events before test
defer cleanupOutboxHelper(ctx, store)()
// Create a new adjustment for this test
newAdj := models.PaymentInitiationAdjustment{
ID: models.PaymentInitiationAdjustmentID{
PaymentInitiationID: defaultPaymentInitiations()[0].ID,
CreatedAt: now.Add(-2 * time.Minute).UTC().Time,
Status: models.PAYMENT_INITIATION_ADJUSTMENT_STATUS_PROCESSING,
},
CreatedAt: now.Add(-2 * time.Minute).UTC().Time,
Status: models.PAYMENT_INITIATION_ADJUSTMENT_STATUS_PROCESSING,
Amount: big.NewInt(500),
Asset: pointer.For("GBP/2"),
Metadata: map[string]string{
"test": "outbox",
},
}
expectedKey := newAdj.IdempotencyKey()
require.NoError(t, store.PaymentInitiationAdjustmentsUpsert(ctx, newAdj))
// Verify outbox event was created
pendingEvents, err := store.OutboxEventsPollPending(ctx, 100)
require.NoError(t, err)
// Find our event
var ourEvent *models.OutboxEvent
for i := range pendingEvents {
if pendingEvents[i].EventType == events.EventTypeSavedPaymentInitiationAdjustment &&
pendingEvents[i].EntityID == newAdj.ID.String() &&
pendingEvents[i].ID.EventIdempotencyKey == expectedKey {
ourEvent = &pendingEvents[i]
break
}
}
require.NotNil(t, ourEvent, "expected outbox event for payment initiation adjustment saved")
// Verify event details
assert.Equal(t, events.EventTypeSavedPaymentInitiationAdjustment, ourEvent.EventType)
assert.Equal(t, models.OUTBOX_STATUS_PENDING, ourEvent.Status)
assert.Equal(t, newAdj.ID.String(), ourEvent.EntityID)
assert.Equal(t, newAdj.ID.PaymentInitiationID.ConnectorID, *ourEvent.ConnectorID)
assert.Equal(t, 0, ourEvent.RetryCount)
assert.Equal(t, expectedKey, ourEvent.ID.EventIdempotencyKey)
// Verify payload
var payload map[string]interface{}
err = json.Unmarshal(ourEvent.Payload, &payload)
require.NoError(t, err)
assert.Equal(t, newAdj.ID.String(), payload["id"])
assert.Equal(t, newAdj.ID.PaymentInitiationID.String(), payload["paymentInitiationID"])
assert.Equal(t, newAdj.Status.String(), payload["status"])
assert.Equal(t, newAdj.Amount.String(), payload["amount"])
assert.Equal(t, *newAdj.Asset, payload["asset"])
// Metadata is unmarshaled as map[string]interface{}, so we need to compare values
payloadMetadata, ok := payload["metadata"].(map[string]interface{})
require.True(t, ok, "metadata should be a map")
assert.Equal(t, newAdj.Metadata["test"], payloadMetadata["test"])
})
t.Run("no outbox event for existing adjustment update", func(t *testing.T) {
// Clean up outbox events before test
defer cleanupOutboxHelper(ctx, store)()
// Count events before
eventsBefore, err := store.OutboxEventsPollPending(ctx, 1000)
require.NoError(t, err)
countBefore := len(eventsBefore)
// Update existing adjustment (should not create event)
existingAdj := defaultPaymentInitiationAdjustments()[0]
existingAdj.Metadata = map[string]string{"updated": "true"}
require.NoError(t, store.PaymentInitiationAdjustmentsUpsert(ctx, existingAdj))
// Verify no new outbox event was created
eventsAfter, err := store.OutboxEventsPollPending(ctx, 1000)
require.NoError(t, err)
countAfter := len(eventsAfter)
// Should have same number of events (no new adjustment saved event)
assert.Equal(t, countBefore, countAfter, "updating existing adjustment should not create saved event")
})
}
func TestPaymentInitiationAdjustmentsUpsertIfStatusEqual(t *testing.T) {
t.Parallel()
ctx := logging.TestingContext()
store := newStore(t)
defer store.Close()
upsertConnector(t, ctx, store, defaultConnector)
upsertAccounts(t, ctx, store, defaultAccounts())
upsertPayments(t, ctx, store, defaultPayments())
upsertPaymentInitiations(t, ctx, store, defaultPaymentInitiations())
upsertPaymentInitiationAdjustments(t, ctx, store, defaultPaymentInitiationAdjustments())
t.Run("upsert with status not equal", func(t *testing.T) {
p := models.PaymentInitiationAdjustment{
ID: models.PaymentInitiationAdjustmentID{
PaymentInitiationID: defaultPaymentInitiations()[1].ID,
CreatedAt: now.UTC().Time,
Status: models.PAYMENT_INITIATION_ADJUSTMENT_STATUS_PROCESSING,
},
CreatedAt: now.UTC().Time,
Status: models.PAYMENT_INITIATION_ADJUSTMENT_STATUS_PROCESSING,
Metadata: map[string]string{
"foo": "bar",
},
}
inserted, err := store.PaymentInitiationAdjustmentsUpsertIfPredicate(
ctx,
p,
func(previous models.PaymentInitiationAdjustment) bool {
return previous.Status == models.PAYMENT_INITIATION_ADJUSTMENT_STATUS_WAITING_FOR_VALIDATION
},
)
require.NoError(t, err)
require.False(t, inserted)
})
t.Run("upsert with status equal", func(t *testing.T) {
// Clean up outbox events before test
defer cleanupOutboxHelper(ctx, store)()
p := models.PaymentInitiationAdjustment{
ID: models.PaymentInitiationAdjustmentID{
PaymentInitiationID: defaultPaymentInitiations()[0].ID,
CreatedAt: now.UTC().Time,
Status: models.PAYMENT_INITIATION_ADJUSTMENT_STATUS_PROCESSING,
},
CreatedAt: now.UTC().Time,
Status: models.PAYMENT_INITIATION_ADJUSTMENT_STATUS_PROCESSING,
Amount: big.NewInt(1000),
Asset: pointer.For("USD/2"),
Metadata: map[string]string{
"foo": "bar",
},
}
expectedKey := p.IdempotencyKey()
inserted, err := store.PaymentInitiationAdjustmentsUpsertIfPredicate(
ctx,
p,
func(previous models.PaymentInitiationAdjustment) bool {
return previous.Status == models.PAYMENT_INITIATION_ADJUSTMENT_STATUS_FAILED
},
)
require.NoError(t, err)
require.True(t, inserted)
// Verify outbox event was created
pendingEvents, err := store.OutboxEventsPollPending(ctx, 100)
require.NoError(t, err)
// Find our event
var ourEvent *models.OutboxEvent
for i := range pendingEvents {
if pendingEvents[i].EventType == events.EventTypeSavedPaymentInitiationAdjustment &&
pendingEvents[i].EntityID == p.ID.String() &&
pendingEvents[i].ID.EventIdempotencyKey == expectedKey {
ourEvent = &pendingEvents[i]
break
}
}
require.NotNil(t, ourEvent, "expected outbox event for payment initiation adjustment saved")
// Verify event details
assert.Equal(t, events.EventTypeSavedPaymentInitiationAdjustment, ourEvent.EventType)
assert.Equal(t, models.OUTBOX_STATUS_PENDING, ourEvent.Status)
assert.Equal(t, p.ID.String(), ourEvent.EntityID)
assert.Equal(t, p.ID.PaymentInitiationID.ConnectorID, *ourEvent.ConnectorID)
assert.Equal(t, 0, ourEvent.RetryCount)
assert.Equal(t, expectedKey, ourEvent.ID.EventIdempotencyKey)
// Verify payload
var payload map[string]interface{}
err = json.Unmarshal(ourEvent.Payload, &payload)
require.NoError(t, err)
assert.Equal(t, p.ID.String(), payload["id"])
assert.Equal(t, p.ID.PaymentInitiationID.String(), payload["paymentInitiationID"])
assert.Equal(t, p.Status.String(), payload["status"])
assert.Equal(t, p.Amount.String(), payload["amount"])
assert.Equal(t, *p.Asset, payload["asset"])
})
t.Run("no outbox event when predicate fails", func(t *testing.T) {
// Clean up outbox events before test
defer cleanupOutboxHelper(ctx, store)()
// Count events before
eventsBefore, err := store.OutboxEventsPollPending(ctx, 1000)
require.NoError(t, err)
countBefore := len(eventsBefore)
p := models.PaymentInitiationAdjustment{
ID: models.PaymentInitiationAdjustmentID{
PaymentInitiationID: defaultPaymentInitiations()[1].ID,
CreatedAt: now.UTC().Time,
Status: models.PAYMENT_INITIATION_ADJUSTMENT_STATUS_PROCESSING,
},
CreatedAt: now.UTC().Time,
Status: models.PAYMENT_INITIATION_ADJUSTMENT_STATUS_PROCESSING,
Metadata: map[string]string{
"foo": "bar",
},
}
inserted, err := store.PaymentInitiationAdjustmentsUpsertIfPredicate(
ctx,
p,
func(previous models.PaymentInitiationAdjustment) bool {
return previous.Status == models.PAYMENT_INITIATION_ADJUSTMENT_STATUS_WAITING_FOR_VALIDATION
},
)
require.NoError(t, err)
require.False(t, inserted)
// Verify no new outbox event was created
eventsAfter, err := store.OutboxEventsPollPending(ctx, 1000)
require.NoError(t, err)
countAfter := len(eventsAfter)
// Should have same number of events (no new adjustment saved event)
assert.Equal(t, countBefore, countAfter, "predicate failure should not create saved event")
})
}
func TestPaymentInitiationAdjustmentsGet(t *testing.T) {
t.Parallel()
ctx := logging.TestingContext()
store := newStore(t)
defer store.Close()
upsertConnector(t, ctx, store, defaultConnector)
upsertAccounts(t, ctx, store, defaultAccounts())
upsertPayments(t, ctx, store, defaultPayments())
upsertPaymentInitiations(t, ctx, store, defaultPaymentInitiations())
upsertPaymentInitiationAdjustments(t, ctx, store, defaultPaymentInitiationAdjustments())
t.Run("get unknown payment initiation adjustment", func(t *testing.T) {
_, err := store.PaymentInitiationAdjustmentsGet(ctx, models.PaymentInitiationAdjustmentID{})
require.Error(t, err)
require.ErrorIs(t, err, ErrNotFound)
})
t.Run("get existing payment initiation adjustment", func(t *testing.T) {
for _, pa := range defaultPaymentInitiationAdjustments() {
actual, err := store.PaymentInitiationAdjustmentsGet(ctx, pa.ID)
require.NoError(t, err)
comparePaymentInitiationAdjustments(t, pa, *actual)
}
})
}
func TestPaymentInitiationAdjustmentsList(t *testing.T) {
t.Parallel()
ctx := logging.TestingContext()
store := newStore(t)
defer store.Close()
upsertConnector(t, ctx, store, defaultConnector)
upsertAccounts(t, ctx, store, defaultAccounts())
upsertPayments(t, ctx, store, defaultPayments())
upsertPaymentInitiations(t, ctx, store, defaultPaymentInitiations())
upsertPaymentInitiationAdjustments(t, ctx, store, defaultPaymentInitiationAdjustments())
t.Run("list payment initiation adjustments by unknown payment initiation", func(t *testing.T) {
cursor, err := store.PaymentInitiationAdjustmentsList(
ctx,
models.PaymentInitiationID{},
NewListPaymentInitiationAdjustmentsQuery(
paginate.NewPaginatedQueryOptions(PaymentInitiationAdjustmentsQuery{}).
WithPageSize(15),
),
)
require.NoError(t, err)
require.Len(t, cursor.Data, 0)
require.False(t, cursor.HasMore)
})
t.Run("wrong query builder operator when listing by status", func(t *testing.T) {
q := NewListPaymentInitiationAdjustmentsQuery(
paginate.NewPaginatedQueryOptions(PaymentInitiationAdjustmentsQuery{}).
WithPageSize(15).
WithQueryBuilder(query.Lt("status", models.PAYMENT_INITIATION_ADJUSTMENT_STATUS_FAILED)),
)
cursor, err := store.PaymentInitiationAdjustmentsList(ctx, defaultPaymentInitiationAdjustments()[0].ID.PaymentInitiationID, q)
require.Error(t, err)
require.Nil(t, cursor)
})
t.Run("list payment initiation adjustments by status", func(t *testing.T) {
q := NewListPaymentInitiationAdjustmentsQuery(
paginate.NewPaginatedQueryOptions(PaymentInitiationAdjustmentsQuery{}).
WithPageSize(15).
WithQueryBuilder(query.Match("status", models.PAYMENT_INITIATION_ADJUSTMENT_STATUS_FAILED)),
)
cursor, err := store.PaymentInitiationAdjustmentsList(ctx, defaultPaymentInitiationAdjustments()[0].ID.PaymentInitiationID, q)
require.NoError(t, err)
require.Len(t, cursor.Data, 1)
require.False(t, cursor.HasMore)
comparePaymentInitiationAdjustments(t, defaultPaymentInitiationAdjustments()[1], cursor.Data[0])
})
t.Run("wrong query builder operator when listing by metadata", func(t *testing.T) {
q := NewListPaymentInitiationAdjustmentsQuery(
paginate.NewPaginatedQueryOptions(PaymentInitiationAdjustmentsQuery{}).
WithPageSize(15).
WithQueryBuilder(query.Lt("metadata[foo]", "bar")),
)
cursor, err := store.PaymentInitiationAdjustmentsList(ctx, defaultPaymentInitiationAdjustments()[0].ID.PaymentInitiationID, q)
require.Error(t, err)
require.Nil(t, cursor)
})
t.Run("list payment initiation adjustments by metadata", func(t *testing.T) {
q := NewListPaymentInitiationAdjustmentsQuery(
paginate.NewPaginatedQueryOptions(PaymentInitiationAdjustmentsQuery{}).
WithPageSize(15).
WithQueryBuilder(query.Match("metadata[foo]", "bar")),
)
cursor, err := store.PaymentInitiationAdjustmentsList(ctx, defaultPaymentInitiationAdjustments()[0].ID.PaymentInitiationID, q)
require.NoError(t, err)
require.Len(t, cursor.Data, 1)
require.False(t, cursor.HasMore)
comparePaymentInitiationAdjustments(t, defaultPaymentInitiationAdjustments()[0], cursor.Data[0])
})
t.Run("unknown query builder key when listing", func(t *testing.T) {
q := NewListPaymentInitiationAdjustmentsQuery(
paginate.NewPaginatedQueryOptions(PaymentInitiationAdjustmentsQuery{}).
WithPageSize(15).
WithQueryBuilder(query.Match("unknown", "test1")),
)
cursor, err := store.PaymentInitiationAdjustmentsList(ctx, defaultPaymentInitiationAdjustments()[0].ID.PaymentInitiationID, q)
require.Error(t, err)
require.Nil(t, cursor)
})
t.Run("list payment initiation adjustments by payment initiation", func(t *testing.T) {
q := NewListPaymentInitiationAdjustmentsQuery(
paginate.NewPaginatedQueryOptions(PaymentInitiationAdjustmentsQuery{}).
WithPageSize(1),
)
cursor, err := store.PaymentInitiationAdjustmentsList(ctx, defaultPaymentInitiationAdjustments()[0].ID.PaymentInitiationID, q)
require.NoError(t, err)
require.Len(t, cursor.Data, 1)
require.True(t, cursor.HasMore)
require.Empty(t, cursor.Previous)
require.NotEmpty(t, cursor.Next)
comparePaymentInitiationAdjustments(t, defaultPaymentInitiationAdjustments()[1], cursor.Data[0])
err = paginate.UnmarshalCursor(cursor.Next, &q)
require.NoError(t, err)
cursor, err = store.PaymentInitiationAdjustmentsList(ctx, defaultPaymentInitiationAdjustments()[0].ID.PaymentInitiationID, q)
require.NoError(t, err)
require.Len(t, cursor.Data, 1)
require.False(t, cursor.HasMore)
require.NotEmpty(t, cursor.Previous)
require.Empty(t, cursor.Next)
comparePaymentInitiationAdjustments(t, defaultPaymentInitiationAdjustments()[0], cursor.Data[0])
err = paginate.UnmarshalCursor(cursor.Previous, &q)
require.NoError(t, err)
cursor, err = store.PaymentInitiationAdjustmentsList(ctx, defaultPaymentInitiationAdjustments()[0].ID.PaymentInitiationID, q)
require.NoError(t, err)
require.Len(t, cursor.Data, 1)
require.True(t, cursor.HasMore)
require.Empty(t, cursor.Previous)
require.NotEmpty(t, cursor.Next)
comparePaymentInitiationAdjustments(t, defaultPaymentInitiationAdjustments()[1], cursor.Data[0])
})
}
func comparePaymentInitiations(t *testing.T, expected, actual models.PaymentInitiation) {
require.Equal(t, expected.ID, actual.ID)
require.Equal(t, expected.ConnectorID, actual.ConnectorID)
require.Equal(t, expected.Reference, actual.Reference)
require.Equal(t, expected.CreatedAt, actual.CreatedAt)
require.Equal(t, expected.ScheduledAt, actual.ScheduledAt)
require.Equal(t, expected.Description, actual.Description)
require.Equal(t, expected.Type, actual.Type)
switch {
case expected.SourceAccountID != nil && actual.SourceAccountID != nil:
require.Equal(t, *expected.SourceAccountID, *actual.SourceAccountID)
case expected.SourceAccountID == nil && actual.SourceAccountID == nil:
default:
t.Fatalf("expected.SourceAccountID != actual.SourceAccountID")
}
require.Equal(t, expected.DestinationAccountID, actual.DestinationAccountID)
require.Equal(t, expected.Amount, actual.Amount)
require.Equal(t, expected.Asset, actual.Asset)
require.Equal(t, len(expected.Metadata), len(actual.Metadata))
for k, v := range expected.Metadata {
_, ok := actual.Metadata[k]
require.True(t, ok)
require.Equal(t, v, actual.Metadata[k])
}
}
func comparePaymentInitiationAdjustments(t *testing.T, expected, actual models.PaymentInitiationAdjustment) {
require.Equal(t, expected.ID, actual.ID)
require.Equal(t, expected.CreatedAt, actual.CreatedAt)
require.Equal(t, expected.Status, actual.Status)
switch {
case expected.Error != nil && actual.Error != nil:
require.Equal(t, expected.Error.Error(), actual.Error.Error())
case expected.Error == nil && actual.Error == nil:
default:
t.Fatalf("expected.Error != actual.Error")
}
require.Equal(t, len(expected.Metadata), len(actual.Metadata))
for k, v := range expected.Metadata {
_, ok := actual.Metadata[k]
require.True(t, ok)
require.Equal(t, v, actual.Metadata[k])
}
}