283 lines
8.7 KiB
Go
283 lines
8.7 KiB
Go
package ledger
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/tech/sendico/ledger/storage"
|
|
"github.com/tech/sendico/ledger/storage/model"
|
|
"github.com/tech/sendico/pkg/merrors"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
"go.mongodb.org/mongo-driver/bson/primitive"
|
|
"go.uber.org/zap"
|
|
)
|
|
|
|
type stubRepository struct {
|
|
accounts storage.AccountsStore
|
|
balances storage.BalancesStore
|
|
outbox storage.OutboxStore
|
|
}
|
|
|
|
func (s *stubRepository) Ping(context.Context) error { return nil }
|
|
func (s *stubRepository) Accounts() storage.AccountsStore { return s.accounts }
|
|
func (s *stubRepository) JournalEntries() storage.JournalEntriesStore { return nil }
|
|
func (s *stubRepository) PostingLines() storage.PostingLinesStore { return nil }
|
|
func (s *stubRepository) Balances() storage.BalancesStore { return s.balances }
|
|
func (s *stubRepository) Outbox() storage.OutboxStore { return s.outbox }
|
|
|
|
type stubAccountsStore struct {
|
|
getByID map[primitive.ObjectID]*model.Account
|
|
defaultSettlement *model.Account
|
|
getErr error
|
|
defaultErr error
|
|
}
|
|
|
|
func (s *stubAccountsStore) Create(context.Context, *model.Account) error {
|
|
return merrors.NotImplemented("create")
|
|
}
|
|
func (s *stubAccountsStore) Get(ctx context.Context, accountRef primitive.ObjectID) (*model.Account, error) {
|
|
if s.getErr != nil {
|
|
return nil, s.getErr
|
|
}
|
|
if acc, ok := s.getByID[accountRef]; ok {
|
|
return acc, nil
|
|
}
|
|
return nil, storage.ErrAccountNotFound
|
|
}
|
|
func (s *stubAccountsStore) GetByAccountCode(context.Context, primitive.ObjectID, string, string) (*model.Account, error) {
|
|
return nil, merrors.NotImplemented("get by code")
|
|
}
|
|
func (s *stubAccountsStore) GetDefaultSettlement(context.Context, primitive.ObjectID, string) (*model.Account, error) {
|
|
if s.defaultErr != nil {
|
|
return nil, s.defaultErr
|
|
}
|
|
if s.defaultSettlement == nil {
|
|
return nil, storage.ErrAccountNotFound
|
|
}
|
|
return s.defaultSettlement, nil
|
|
}
|
|
func (s *stubAccountsStore) ListByOrganization(context.Context, primitive.ObjectID, int, int) ([]*model.Account, error) {
|
|
return nil, merrors.NotImplemented("list")
|
|
}
|
|
func (s *stubAccountsStore) UpdateStatus(context.Context, primitive.ObjectID, model.AccountStatus) error {
|
|
return merrors.NotImplemented("update status")
|
|
}
|
|
|
|
type stubBalancesStore struct {
|
|
records map[primitive.ObjectID]*model.AccountBalance
|
|
upserts []*model.AccountBalance
|
|
getErr error
|
|
upErr error
|
|
}
|
|
|
|
func (s *stubBalancesStore) Get(ctx context.Context, accountRef primitive.ObjectID) (*model.AccountBalance, error) {
|
|
if s.getErr != nil {
|
|
return nil, s.getErr
|
|
}
|
|
if balance, ok := s.records[accountRef]; ok {
|
|
return balance, nil
|
|
}
|
|
return nil, storage.ErrBalanceNotFound
|
|
}
|
|
|
|
func (s *stubBalancesStore) Upsert(ctx context.Context, balance *model.AccountBalance) error {
|
|
if s.upErr != nil {
|
|
return s.upErr
|
|
}
|
|
copied := *balance
|
|
s.upserts = append(s.upserts, &copied)
|
|
if s.records == nil {
|
|
s.records = make(map[primitive.ObjectID]*model.AccountBalance)
|
|
}
|
|
s.records[balance.AccountRef] = &copied
|
|
return nil
|
|
}
|
|
|
|
func (s *stubBalancesStore) IncrementBalance(context.Context, primitive.ObjectID, string) error {
|
|
return merrors.NotImplemented("increment")
|
|
}
|
|
|
|
type stubOutboxStore struct {
|
|
created []*model.OutboxEvent
|
|
err error
|
|
}
|
|
|
|
func (s *stubOutboxStore) Create(ctx context.Context, event *model.OutboxEvent) error {
|
|
if s.err != nil {
|
|
return s.err
|
|
}
|
|
copied := *event
|
|
s.created = append(s.created, &copied)
|
|
return nil
|
|
}
|
|
|
|
func (s *stubOutboxStore) ListPending(context.Context, int) ([]*model.OutboxEvent, error) {
|
|
return nil, merrors.NotImplemented("list")
|
|
}
|
|
|
|
func (s *stubOutboxStore) MarkSent(context.Context, primitive.ObjectID, time.Time) error {
|
|
return merrors.NotImplemented("mark sent")
|
|
}
|
|
|
|
func (s *stubOutboxStore) MarkFailed(context.Context, primitive.ObjectID) error {
|
|
return merrors.NotImplemented("mark failed")
|
|
}
|
|
|
|
func (s *stubOutboxStore) IncrementAttempts(context.Context, primitive.ObjectID) error {
|
|
return merrors.NotImplemented("increment attempts")
|
|
}
|
|
|
|
func TestResolveSettlementAccount_Default(t *testing.T) {
|
|
ctx := context.Background()
|
|
orgRef := primitive.NewObjectID()
|
|
settlementID := primitive.NewObjectID()
|
|
settlement := &model.Account{}
|
|
settlement.SetID(settlementID)
|
|
settlement.OrganizationRef = orgRef
|
|
settlement.Currency = "USD"
|
|
settlement.Status = model.AccountStatusActive
|
|
|
|
accounts := &stubAccountsStore{defaultSettlement: settlement}
|
|
repo := &stubRepository{accounts: accounts}
|
|
service := &Service{logger: zap.NewNop(), storage: repo}
|
|
cache := make(map[primitive.ObjectID]*model.Account)
|
|
|
|
result, err := service.resolveSettlementAccount(ctx, orgRef, "USD", "", cache)
|
|
|
|
require.NoError(t, err)
|
|
assert.Equal(t, settlement, result)
|
|
assert.Equal(t, settlement, cache[settlementID])
|
|
}
|
|
|
|
func TestResolveSettlementAccount_Override(t *testing.T) {
|
|
ctx := context.Background()
|
|
orgRef := primitive.NewObjectID()
|
|
overrideID := primitive.NewObjectID()
|
|
override := &model.Account{}
|
|
override.SetID(overrideID)
|
|
override.OrganizationRef = orgRef
|
|
override.Currency = "EUR"
|
|
override.Status = model.AccountStatusActive
|
|
|
|
accounts := &stubAccountsStore{getByID: map[primitive.ObjectID]*model.Account{overrideID: override}}
|
|
repo := &stubRepository{accounts: accounts}
|
|
service := &Service{logger: zap.NewNop(), storage: repo}
|
|
cache := make(map[primitive.ObjectID]*model.Account)
|
|
|
|
result, err := service.resolveSettlementAccount(ctx, orgRef, "EUR", overrideID.Hex(), cache)
|
|
|
|
require.NoError(t, err)
|
|
assert.Equal(t, override, result)
|
|
assert.Equal(t, override, cache[overrideID])
|
|
}
|
|
|
|
func TestResolveSettlementAccount_NoDefault(t *testing.T) {
|
|
ctx := context.Background()
|
|
orgRef := primitive.NewObjectID()
|
|
accounts := &stubAccountsStore{defaultErr: storage.ErrAccountNotFound}
|
|
repo := &stubRepository{accounts: accounts}
|
|
service := &Service{logger: zap.NewNop(), storage: repo}
|
|
|
|
_, err := service.resolveSettlementAccount(ctx, orgRef, "USD", "", map[primitive.ObjectID]*model.Account{})
|
|
|
|
require.Error(t, err)
|
|
assert.True(t, errors.Is(err, merrors.ErrInvalidArg))
|
|
}
|
|
|
|
func TestUpsertBalances_Succeeds(t *testing.T) {
|
|
ctx := context.Background()
|
|
orgRef := primitive.NewObjectID()
|
|
accountRef := primitive.NewObjectID()
|
|
account := &model.Account{AllowNegative: false, Currency: "USD"}
|
|
account.OrganizationRef = orgRef
|
|
|
|
balanceLines := []*model.PostingLine{
|
|
{
|
|
AccountRef: accountRef,
|
|
Amount: "50",
|
|
Currency: "USD",
|
|
},
|
|
}
|
|
|
|
balances := &stubBalancesStore{}
|
|
repo := &stubRepository{balances: balances}
|
|
service := &Service{logger: zap.NewNop(), storage: repo}
|
|
accountCache := map[primitive.ObjectID]*model.Account{accountRef: account}
|
|
|
|
require.NoError(t, service.upsertBalances(ctx, balanceLines, accountCache))
|
|
require.Len(t, balances.upserts, 1)
|
|
assert.Equal(t, "50", balances.upserts[0].Balance)
|
|
assert.Equal(t, int64(1), balances.upserts[0].Version)
|
|
assert.Equal(t, "USD", balances.upserts[0].Currency)
|
|
}
|
|
|
|
func TestUpsertBalances_DisallowNegative(t *testing.T) {
|
|
ctx := context.Background()
|
|
orgRef := primitive.NewObjectID()
|
|
accountRef := primitive.NewObjectID()
|
|
account := &model.Account{AllowNegative: false, Currency: "USD"}
|
|
account.OrganizationRef = orgRef
|
|
|
|
balanceLines := []*model.PostingLine{
|
|
{
|
|
AccountRef: accountRef,
|
|
Amount: "-10",
|
|
Currency: "USD",
|
|
},
|
|
}
|
|
|
|
balances := &stubBalancesStore{}
|
|
repo := &stubRepository{balances: balances}
|
|
service := &Service{logger: zap.NewNop(), storage: repo}
|
|
accountCache := map[primitive.ObjectID]*model.Account{accountRef: account}
|
|
|
|
err := service.upsertBalances(ctx, balanceLines, accountCache)
|
|
|
|
require.Error(t, err)
|
|
assert.True(t, errors.Is(err, merrors.ErrInvalidArg))
|
|
}
|
|
|
|
func TestEnqueueOutbox_CreatesEvent(t *testing.T) {
|
|
ctx := context.Background()
|
|
orgRef := primitive.NewObjectID()
|
|
entryID := primitive.NewObjectID()
|
|
entry := &model.JournalEntry{
|
|
IdempotencyKey: "idem",
|
|
EventTime: time.Now().UTC(),
|
|
EntryType: model.EntryTypeCredit,
|
|
Version: 42,
|
|
}
|
|
entry.OrganizationRef = orgRef
|
|
entry.SetID(entryID)
|
|
|
|
lines := []*model.PostingLine{
|
|
{
|
|
AccountRef: primitive.NewObjectID(),
|
|
Amount: "100",
|
|
Currency: "USD",
|
|
LineType: model.LineTypeMain,
|
|
},
|
|
}
|
|
|
|
producer := &stubOutboxStore{}
|
|
repo := &stubRepository{outbox: producer}
|
|
service := &Service{logger: zap.NewNop(), storage: repo}
|
|
|
|
require.NoError(t, service.enqueueOutbox(ctx, entry, lines))
|
|
require.Len(t, producer.created, 1)
|
|
event := producer.created[0]
|
|
assert.Equal(t, entryID.Hex(), event.EventID)
|
|
assert.Equal(t, ledgerOutboxSubject, event.Subject)
|
|
|
|
var payload outboxJournalPayload
|
|
require.NoError(t, json.Unmarshal(event.Payload, &payload))
|
|
assert.Equal(t, entryID.Hex(), payload.JournalEntryRef)
|
|
assert.Equal(t, "credit", payload.EntryType)
|
|
assert.Len(t, payload.Lines, 1)
|
|
assert.Equal(t, "100", payload.Lines[0].Amount)
|
|
}
|