508 lines
17 KiB
Go
508 lines
17 KiB
Go
package ledger
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"math/rand"
|
|
"strconv"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/shopspring/decimal"
|
|
"github.com/stretchr/testify/require"
|
|
"github.com/tech/sendico/ledger/storage"
|
|
"github.com/tech/sendico/ledger/storage/model"
|
|
"github.com/tech/sendico/pkg/db/transaction"
|
|
"github.com/tech/sendico/pkg/merrors"
|
|
pmodel "github.com/tech/sendico/pkg/model"
|
|
"github.com/tech/sendico/pkg/model/account_role"
|
|
moneyv1 "github.com/tech/sendico/pkg/proto/common/money/v1"
|
|
ledgerv1 "github.com/tech/sendico/pkg/proto/ledger/v1"
|
|
"go.mongodb.org/mongo-driver/v2/bson"
|
|
"go.uber.org/zap"
|
|
)
|
|
|
|
type memoryTxFactory struct{}
|
|
|
|
func (memoryTxFactory) CreateTransaction() transaction.Transaction { return memoryTx{} }
|
|
|
|
type memoryTx struct{}
|
|
|
|
func (memoryTx) Execute(ctx context.Context, cb transaction.Callback) (any, error) {
|
|
return cb(ctx)
|
|
}
|
|
|
|
type memoryRepository struct {
|
|
accounts *memoryAccountsStore
|
|
journalEntries *memoryJournalEntriesStore
|
|
postingLines *memoryPostingLinesStore
|
|
balances *memoryBalancesStore
|
|
outbox *memoryOutboxStore
|
|
txFactory transaction.Factory
|
|
}
|
|
|
|
func (r *memoryRepository) Ping(context.Context) error { return nil }
|
|
func (r *memoryRepository) Accounts() storage.AccountsStore { return r.accounts }
|
|
func (r *memoryRepository) JournalEntries() storage.JournalEntriesStore { return r.journalEntries }
|
|
func (r *memoryRepository) PostingLines() storage.PostingLinesStore { return r.postingLines }
|
|
func (r *memoryRepository) Balances() storage.BalancesStore { return r.balances }
|
|
func (r *memoryRepository) Outbox() storage.OutboxStore { return r.outbox }
|
|
func (r *memoryRepository) TransactionFactory() transaction.Factory { return r.txFactory }
|
|
|
|
type memoryAccountsStore struct {
|
|
records map[bson.ObjectID]*pmodel.LedgerAccount
|
|
systemByPurposeKey map[string]*pmodel.LedgerAccount
|
|
}
|
|
|
|
func (s *memoryAccountsStore) Create(_ context.Context, account *pmodel.LedgerAccount) error {
|
|
if account.GetID() == nil || account.GetID().IsZero() {
|
|
account.SetID(bson.NewObjectID())
|
|
}
|
|
if s.records == nil {
|
|
s.records = make(map[bson.ObjectID]*pmodel.LedgerAccount)
|
|
}
|
|
s.records[*account.GetID()] = account
|
|
if account.SystemPurpose != nil {
|
|
if s.systemByPurposeKey == nil {
|
|
s.systemByPurposeKey = make(map[string]*pmodel.LedgerAccount)
|
|
}
|
|
key := string(*account.SystemPurpose) + "|" + account.Currency
|
|
s.systemByPurposeKey[key] = account
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (s *memoryAccountsStore) Get(_ context.Context, accountRef bson.ObjectID) (*pmodel.LedgerAccount, error) {
|
|
if s.records == nil {
|
|
return nil, storage.ErrAccountNotFound
|
|
}
|
|
if acc, ok := s.records[accountRef]; ok {
|
|
return acc, nil
|
|
}
|
|
return nil, storage.ErrAccountNotFound
|
|
}
|
|
|
|
func (s *memoryAccountsStore) GetByAccountCode(context.Context, bson.ObjectID, string, string) (*pmodel.LedgerAccount, error) {
|
|
return nil, merrors.NotImplemented("get by code")
|
|
}
|
|
|
|
func (s *memoryAccountsStore) GetByRole(context.Context, bson.ObjectID, string, account_role.AccountRole) (*pmodel.LedgerAccount, error) {
|
|
return nil, merrors.NotImplemented("get by role")
|
|
}
|
|
|
|
func (s *memoryAccountsStore) GetSystemAccount(_ context.Context, purpose pmodel.SystemAccountPurpose, currency string) (*pmodel.LedgerAccount, error) {
|
|
if s.systemByPurposeKey == nil {
|
|
return nil, storage.ErrAccountNotFound
|
|
}
|
|
key := string(purpose) + "|" + currency
|
|
if acc, ok := s.systemByPurposeKey[key]; ok {
|
|
return acc, nil
|
|
}
|
|
return nil, storage.ErrAccountNotFound
|
|
}
|
|
|
|
func (s *memoryAccountsStore) GetDefaultSettlement(context.Context, bson.ObjectID, string) (*pmodel.LedgerAccount, error) {
|
|
return nil, merrors.NotImplemented("get default settlement")
|
|
}
|
|
|
|
func (s *memoryAccountsStore) ListByOrganization(context.Context, bson.ObjectID, *storage.AccountsFilter, int, int) ([]*pmodel.LedgerAccount, error) {
|
|
return nil, merrors.NotImplemented("list by organization")
|
|
}
|
|
|
|
func (s *memoryAccountsStore) UpdateStatus(context.Context, bson.ObjectID, pmodel.LedgerAccountStatus) error {
|
|
return merrors.NotImplemented("update status")
|
|
}
|
|
|
|
func (s *memoryAccountsStore) ListByCurrency(_ context.Context, currency string) ([]*pmodel.LedgerAccount, error) {
|
|
accounts := make([]*pmodel.LedgerAccount, 0)
|
|
for _, acc := range s.records {
|
|
if acc == nil {
|
|
continue
|
|
}
|
|
if acc.Currency != currency {
|
|
continue
|
|
}
|
|
if acc.Scope != "" && acc.Scope != pmodel.LedgerAccountScopeOrganization {
|
|
continue
|
|
}
|
|
accounts = append(accounts, acc)
|
|
}
|
|
return accounts, nil
|
|
}
|
|
|
|
type memoryJournalEntriesStore struct {
|
|
byKey map[string]*model.JournalEntry
|
|
}
|
|
|
|
func (s *memoryJournalEntriesStore) Create(_ context.Context, entry *model.JournalEntry) error {
|
|
if entry.GetID() == nil || entry.GetID().IsZero() {
|
|
entry.SetID(bson.NewObjectID())
|
|
}
|
|
if s.byKey == nil {
|
|
s.byKey = make(map[string]*model.JournalEntry)
|
|
}
|
|
key := entry.OrganizationRef.Hex() + "|" + entry.IdempotencyKey
|
|
s.byKey[key] = entry
|
|
return nil
|
|
}
|
|
|
|
func (s *memoryJournalEntriesStore) Get(context.Context, bson.ObjectID) (*model.JournalEntry, error) {
|
|
return nil, merrors.NotImplemented("get entry")
|
|
}
|
|
|
|
func (s *memoryJournalEntriesStore) GetByIdempotencyKey(_ context.Context, orgRef bson.ObjectID, key string) (*model.JournalEntry, error) {
|
|
if s.byKey == nil {
|
|
return nil, storage.ErrJournalEntryNotFound
|
|
}
|
|
entry, ok := s.byKey[orgRef.Hex()+"|"+key]
|
|
if !ok {
|
|
return nil, storage.ErrJournalEntryNotFound
|
|
}
|
|
return entry, nil
|
|
}
|
|
|
|
func (s *memoryJournalEntriesStore) ListByOrganization(context.Context, bson.ObjectID, int, int) ([]*model.JournalEntry, error) {
|
|
return nil, merrors.NotImplemented("list entries")
|
|
}
|
|
|
|
type memoryPostingLinesStore struct {
|
|
lines []*model.PostingLine
|
|
}
|
|
|
|
func (s *memoryPostingLinesStore) CreateMany(_ context.Context, lines []*model.PostingLine) error {
|
|
s.lines = append(s.lines, lines...)
|
|
return nil
|
|
}
|
|
|
|
func (s *memoryPostingLinesStore) ListByJournalEntry(context.Context, bson.ObjectID) ([]*model.PostingLine, error) {
|
|
return nil, merrors.NotImplemented("list lines by entry")
|
|
}
|
|
|
|
func (s *memoryPostingLinesStore) ListByAccount(context.Context, bson.ObjectID, int, int) ([]*model.PostingLine, error) {
|
|
return nil, merrors.NotImplemented("list lines by account")
|
|
}
|
|
|
|
type memoryBalancesStore struct {
|
|
records map[bson.ObjectID]*model.AccountBalance
|
|
}
|
|
|
|
func (s *memoryBalancesStore) Get(_ context.Context, accountRef bson.ObjectID) (*model.AccountBalance, error) {
|
|
if s.records == nil {
|
|
return nil, storage.ErrBalanceNotFound
|
|
}
|
|
if balance, ok := s.records[accountRef]; ok {
|
|
copied := *balance
|
|
return &copied, nil
|
|
}
|
|
return nil, storage.ErrBalanceNotFound
|
|
}
|
|
|
|
func (s *memoryBalancesStore) Upsert(_ context.Context, balance *model.AccountBalance) error {
|
|
if s.records == nil {
|
|
s.records = make(map[bson.ObjectID]*model.AccountBalance)
|
|
}
|
|
copied := *balance
|
|
s.records[balance.AccountRef] = &copied
|
|
return nil
|
|
}
|
|
|
|
func (s *memoryBalancesStore) IncrementBalance(context.Context, bson.ObjectID, string) error {
|
|
return merrors.NotImplemented("increment balance")
|
|
}
|
|
|
|
type memoryOutboxStore struct{}
|
|
|
|
func (memoryOutboxStore) Create(context.Context, *model.OutboxEvent) error { return nil }
|
|
func (memoryOutboxStore) ListPending(context.Context, int) ([]*model.OutboxEvent, error) {
|
|
return nil, merrors.NotImplemented("list outbox")
|
|
}
|
|
func (memoryOutboxStore) MarkSent(context.Context, bson.ObjectID, time.Time) error {
|
|
return merrors.NotImplemented("mark sent")
|
|
}
|
|
func (memoryOutboxStore) MarkFailed(context.Context, bson.ObjectID) error {
|
|
return merrors.NotImplemented("mark failed")
|
|
}
|
|
func (memoryOutboxStore) IncrementAttempts(context.Context, bson.ObjectID) error {
|
|
return merrors.NotImplemented("increment attempts")
|
|
}
|
|
|
|
func newTestService() (*Service, *memoryRepository) {
|
|
repo := &memoryRepository{
|
|
accounts: &memoryAccountsStore{},
|
|
journalEntries: &memoryJournalEntriesStore{},
|
|
postingLines: &memoryPostingLinesStore{},
|
|
balances: &memoryBalancesStore{},
|
|
outbox: &memoryOutboxStore{},
|
|
txFactory: memoryTxFactory{},
|
|
}
|
|
svc := &Service{
|
|
logger: zap.NewNop(),
|
|
storage: repo,
|
|
}
|
|
return svc, repo
|
|
}
|
|
|
|
func newOrgAccount(orgRef bson.ObjectID, currency string, role account_role.AccountRole) *pmodel.LedgerAccount {
|
|
account := &pmodel.LedgerAccount{
|
|
AccountCode: "test:" + strings.ToLower(currency) + ":" + bson.NewObjectID().Hex(),
|
|
Currency: currency,
|
|
AccountType: pmodel.LedgerAccountTypeAsset,
|
|
Status: pmodel.LedgerAccountStatusActive,
|
|
AllowNegative: false,
|
|
Role: role,
|
|
Scope: pmodel.LedgerAccountScopeOrganization,
|
|
}
|
|
account.OrganizationRef = &orgRef
|
|
return account
|
|
}
|
|
|
|
func balanceString(t *testing.T, balances *memoryBalancesStore, accountID bson.ObjectID) string {
|
|
t.Helper()
|
|
bal, err := balances.Get(context.Background(), accountID)
|
|
if errors.Is(err, storage.ErrBalanceNotFound) {
|
|
return "0"
|
|
}
|
|
require.NoError(t, err)
|
|
return bal.Balance
|
|
}
|
|
|
|
func balanceDecimal(t *testing.T, balances *memoryBalancesStore, accountID bson.ObjectID) decimal.Decimal {
|
|
t.Helper()
|
|
bal, err := balances.Get(context.Background(), accountID)
|
|
if errors.Is(err, storage.ErrBalanceNotFound) {
|
|
return decimal.Zero
|
|
}
|
|
require.NoError(t, err)
|
|
dec, err := decimal.NewFromString(bal.Balance)
|
|
require.NoError(t, err)
|
|
return dec
|
|
}
|
|
|
|
func TestExternalCreditAndDebit(t *testing.T) {
|
|
originalCurrencies := pmodel.SupportedCurrencies
|
|
pmodel.SupportedCurrencies = []pmodel.Currency{pmodel.CurrencyUSD}
|
|
t.Cleanup(func() {
|
|
pmodel.SupportedCurrencies = originalCurrencies
|
|
})
|
|
|
|
ctx := context.Background()
|
|
svc, repo := newTestService()
|
|
require.NoError(t, svc.ensureSystemAccounts(ctx))
|
|
|
|
orgRef := bson.NewObjectID()
|
|
pending := newOrgAccount(orgRef, "USD", account_role.AccountRolePending)
|
|
require.NoError(t, repo.accounts.Create(ctx, pending))
|
|
|
|
creditResp, err := svc.PostExternalCreditWithCharges(ctx, &ledgerv1.PostCreditRequest{
|
|
IdempotencyKey: "external-credit-1",
|
|
OrganizationRef: orgRef.Hex(),
|
|
LedgerAccountRef: pending.GetID().Hex(),
|
|
Money: &moneyv1.Money{Currency: "USD", Amount: "100"},
|
|
})
|
|
require.NoError(t, err)
|
|
require.NotEmpty(t, creditResp.GetJournalEntryRef())
|
|
|
|
source, err := repo.accounts.GetSystemAccount(ctx, pmodel.SystemAccountPurposeExternalSource, "USD")
|
|
require.NoError(t, err)
|
|
require.Equal(t, "100", balanceString(t, repo.balances, *pending.GetID()))
|
|
require.Equal(t, "-100", balanceString(t, repo.balances, *source.GetID()))
|
|
|
|
debitResp, err := svc.PostExternalDebitWithCharges(ctx, &ledgerv1.PostDebitRequest{
|
|
IdempotencyKey: "external-debit-1",
|
|
OrganizationRef: orgRef.Hex(),
|
|
LedgerAccountRef: pending.GetID().Hex(),
|
|
Money: &moneyv1.Money{Currency: "USD", Amount: "40"},
|
|
})
|
|
require.NoError(t, err)
|
|
require.NotEmpty(t, debitResp.GetJournalEntryRef())
|
|
|
|
sink, err := repo.accounts.GetSystemAccount(ctx, pmodel.SystemAccountPurposeExternalSink, "USD")
|
|
require.NoError(t, err)
|
|
require.Equal(t, "60", balanceString(t, repo.balances, *pending.GetID()))
|
|
require.Equal(t, "40", balanceString(t, repo.balances, *sink.GetID()))
|
|
}
|
|
|
|
func TestExternalCreditCurrencyMismatch(t *testing.T) {
|
|
originalCurrencies := pmodel.SupportedCurrencies
|
|
pmodel.SupportedCurrencies = []pmodel.Currency{pmodel.CurrencyUSD}
|
|
t.Cleanup(func() {
|
|
pmodel.SupportedCurrencies = originalCurrencies
|
|
})
|
|
|
|
ctx := context.Background()
|
|
svc, repo := newTestService()
|
|
require.NoError(t, svc.ensureSystemAccounts(ctx))
|
|
|
|
orgRef := bson.NewObjectID()
|
|
pending := newOrgAccount(orgRef, "USD", account_role.AccountRolePending)
|
|
require.NoError(t, repo.accounts.Create(ctx, pending))
|
|
|
|
source, err := repo.accounts.GetSystemAccount(ctx, pmodel.SystemAccountPurposeExternalSource, "USD")
|
|
require.NoError(t, err)
|
|
source.Currency = "EUR"
|
|
|
|
_, err = svc.PostExternalCreditWithCharges(ctx, &ledgerv1.PostCreditRequest{
|
|
IdempotencyKey: "external-credit-mismatch",
|
|
OrganizationRef: orgRef.Hex(),
|
|
LedgerAccountRef: pending.GetID().Hex(),
|
|
Money: &moneyv1.Money{Currency: "USD", Amount: "10"},
|
|
})
|
|
require.Error(t, err)
|
|
}
|
|
|
|
func TestExternalOperationsRejectSystemScopeTargets(t *testing.T) {
|
|
originalCurrencies := pmodel.SupportedCurrencies
|
|
pmodel.SupportedCurrencies = []pmodel.Currency{pmodel.CurrencyUSD}
|
|
t.Cleanup(func() {
|
|
pmodel.SupportedCurrencies = originalCurrencies
|
|
})
|
|
|
|
ctx := context.Background()
|
|
svc, repo := newTestService()
|
|
require.NoError(t, svc.ensureSystemAccounts(ctx))
|
|
|
|
orgRef := bson.NewObjectID()
|
|
source, err := repo.accounts.GetSystemAccount(ctx, pmodel.SystemAccountPurposeExternalSource, "USD")
|
|
require.NoError(t, err)
|
|
sink, err := repo.accounts.GetSystemAccount(ctx, pmodel.SystemAccountPurposeExternalSink, "USD")
|
|
require.NoError(t, err)
|
|
|
|
_, err = svc.PostExternalCreditWithCharges(ctx, &ledgerv1.PostCreditRequest{
|
|
IdempotencyKey: "external-credit-system-target",
|
|
OrganizationRef: orgRef.Hex(),
|
|
LedgerAccountRef: source.GetID().Hex(),
|
|
Money: &moneyv1.Money{Currency: "USD", Amount: "10"},
|
|
})
|
|
require.Error(t, err)
|
|
|
|
_, err = svc.PostExternalDebitWithCharges(ctx, &ledgerv1.PostDebitRequest{
|
|
IdempotencyKey: "external-debit-system-source",
|
|
OrganizationRef: orgRef.Hex(),
|
|
LedgerAccountRef: sink.GetID().Hex(),
|
|
Money: &moneyv1.Money{Currency: "USD", Amount: "10"},
|
|
})
|
|
require.Error(t, err)
|
|
}
|
|
|
|
func TestExternalFlowInvariant(t *testing.T) {
|
|
originalCurrencies := pmodel.SupportedCurrencies
|
|
pmodel.SupportedCurrencies = []pmodel.Currency{pmodel.CurrencyUSD}
|
|
t.Cleanup(func() {
|
|
pmodel.SupportedCurrencies = originalCurrencies
|
|
})
|
|
|
|
ctx := context.Background()
|
|
svc, repo := newTestService()
|
|
require.NoError(t, svc.ensureSystemAccounts(ctx))
|
|
|
|
orgRef := bson.NewObjectID()
|
|
pending := newOrgAccount(orgRef, "USD", account_role.AccountRolePending)
|
|
transit := newOrgAccount(orgRef, "USD", account_role.AccountRoleTransit)
|
|
require.NoError(t, repo.accounts.Create(ctx, pending))
|
|
require.NoError(t, repo.accounts.Create(ctx, transit))
|
|
|
|
_, err := svc.PostExternalCreditWithCharges(ctx, &ledgerv1.PostCreditRequest{
|
|
IdempotencyKey: "flow-credit",
|
|
OrganizationRef: orgRef.Hex(),
|
|
LedgerAccountRef: pending.GetID().Hex(),
|
|
Money: &moneyv1.Money{Currency: "USD", Amount: "100"},
|
|
})
|
|
require.NoError(t, err)
|
|
require.NoError(t, svc.CheckExternalInvariant(ctx, "USD"))
|
|
|
|
_, err = svc.TransferInternal(ctx, &ledgerv1.TransferRequest{
|
|
IdempotencyKey: "flow-move",
|
|
OrganizationRef: orgRef.Hex(),
|
|
FromLedgerAccountRef: pending.GetID().Hex(),
|
|
ToLedgerAccountRef: transit.GetID().Hex(),
|
|
Money: &moneyv1.Money{Currency: "USD", Amount: "100"},
|
|
})
|
|
require.NoError(t, err)
|
|
require.NoError(t, svc.CheckExternalInvariant(ctx, "USD"))
|
|
|
|
_, err = svc.PostExternalDebitWithCharges(ctx, &ledgerv1.PostDebitRequest{
|
|
IdempotencyKey: "flow-debit",
|
|
OrganizationRef: orgRef.Hex(),
|
|
LedgerAccountRef: transit.GetID().Hex(),
|
|
Money: &moneyv1.Money{Currency: "USD", Amount: "40"},
|
|
})
|
|
require.NoError(t, err)
|
|
require.NoError(t, svc.CheckExternalInvariant(ctx, "USD"))
|
|
}
|
|
|
|
func TestExternalInvariantRandomSequence(t *testing.T) {
|
|
originalCurrencies := pmodel.SupportedCurrencies
|
|
pmodel.SupportedCurrencies = []pmodel.Currency{pmodel.CurrencyUSD}
|
|
t.Cleanup(func() {
|
|
pmodel.SupportedCurrencies = originalCurrencies
|
|
})
|
|
|
|
ctx := context.Background()
|
|
svc, repo := newTestService()
|
|
require.NoError(t, svc.ensureSystemAccounts(ctx))
|
|
|
|
orgRef := bson.NewObjectID()
|
|
pending := newOrgAccount(orgRef, "USD", account_role.AccountRolePending)
|
|
transit := newOrgAccount(orgRef, "USD", account_role.AccountRoleTransit)
|
|
require.NoError(t, repo.accounts.Create(ctx, pending))
|
|
require.NoError(t, repo.accounts.Create(ctx, transit))
|
|
|
|
rng := rand.New(rand.NewSource(42))
|
|
for i := 0; i < 50; i++ {
|
|
switch rng.Intn(3) {
|
|
case 0:
|
|
amount := rng.Intn(20) + 1
|
|
_, err := svc.PostExternalCreditWithCharges(ctx, &ledgerv1.PostCreditRequest{
|
|
IdempotencyKey: "rand-credit-" + strconv.Itoa(i),
|
|
OrganizationRef: orgRef.Hex(),
|
|
LedgerAccountRef: pending.GetID().Hex(),
|
|
Money: &moneyv1.Money{Currency: "USD", Amount: strconv.Itoa(amount)},
|
|
})
|
|
require.NoError(t, err)
|
|
case 1:
|
|
sourceID := *pending.GetID()
|
|
destID := *transit.GetID()
|
|
sourceBal := balanceDecimal(t, repo.balances, sourceID)
|
|
if sourceBal.LessThanOrEqual(decimal.Zero) {
|
|
sourceID = *transit.GetID()
|
|
destID = *pending.GetID()
|
|
sourceBal = balanceDecimal(t, repo.balances, sourceID)
|
|
}
|
|
if sourceBal.LessThanOrEqual(decimal.Zero) {
|
|
continue
|
|
}
|
|
max := int(sourceBal.IntPart())
|
|
amount := rng.Intn(max) + 1
|
|
_, err := svc.TransferInternal(ctx, &ledgerv1.TransferRequest{
|
|
IdempotencyKey: "rand-move-" + strconv.Itoa(i),
|
|
OrganizationRef: orgRef.Hex(),
|
|
FromLedgerAccountRef: sourceID.Hex(),
|
|
ToLedgerAccountRef: destID.Hex(),
|
|
Money: &moneyv1.Money{Currency: "USD", Amount: strconv.Itoa(amount)},
|
|
})
|
|
require.NoError(t, err)
|
|
case 2:
|
|
sourceID := *pending.GetID()
|
|
sourceBal := balanceDecimal(t, repo.balances, sourceID)
|
|
if sourceBal.LessThanOrEqual(decimal.Zero) {
|
|
sourceID = *transit.GetID()
|
|
sourceBal = balanceDecimal(t, repo.balances, sourceID)
|
|
}
|
|
if sourceBal.LessThanOrEqual(decimal.Zero) {
|
|
continue
|
|
}
|
|
max := int(sourceBal.IntPart())
|
|
amount := rng.Intn(max) + 1
|
|
_, err := svc.PostExternalDebitWithCharges(ctx, &ledgerv1.PostDebitRequest{
|
|
IdempotencyKey: "rand-debit-" + strconv.Itoa(i),
|
|
OrganizationRef: orgRef.Hex(),
|
|
LedgerAccountRef: sourceID.Hex(),
|
|
Money: &moneyv1.Money{Currency: "USD", Amount: strconv.Itoa(amount)},
|
|
})
|
|
require.NoError(t, err)
|
|
}
|
|
require.NoError(t, svc.CheckExternalInvariant(ctx, "USD"))
|
|
}
|
|
}
|