This commit is contained in:
Stephan D
2026-03-10 12:31:09 +01:00
parent d87e709f43
commit e77d1ab793
287 changed files with 2089 additions and 1550 deletions

View File

@@ -2,6 +2,7 @@ package ledger
import (
"context"
"errors"
"strings"
"github.com/tech/sendico/ledger/storage"
@@ -35,7 +36,7 @@ func (s *Service) blockAccountResponder(_ context.Context, req *ledgerv1.BlockAc
account, err := s.storage.Accounts().Get(ctx, accountRef)
if err != nil {
if err == storage.ErrAccountNotFound {
if errors.Is(err, storage.ErrAccountNotFound) {
return nil, merrors.NoData("account not found")
}
logger.Warn("Failed to get account for block", zap.Error(err))
@@ -98,7 +99,7 @@ func (s *Service) unblockAccountResponder(_ context.Context, req *ledgerv1.Unblo
account, err := s.storage.Accounts().Get(ctx, accountRef)
if err != nil {
if err == storage.ErrAccountNotFound {
if errors.Is(err, storage.ErrAccountNotFound) {
return nil, merrors.NoData("account not found")
}
logger.Warn("Failed to get account for unblock", zap.Error(err))

View File

@@ -188,7 +188,7 @@ func (s *Service) persistNewAccount(ctx context.Context, p createAccountParams,
// parseOwnerRef parses an optional owner reference string into an ObjectID pointer.
func parseOwnerRef(ownerRefStr string) (*bson.ObjectID, error) {
if ownerRefStr == "" {
return nil, nil
return nil, nil //nolint:nilnil // empty owner_ref means owner linkage is intentionally omitted
}
ownerObjID, err := parseObjectID(ownerRefStr)
if err != nil {

View File

@@ -97,7 +97,7 @@ func (s *accountStoreStub) GetDefaultSettlement(context.Context, bson.ObjectID,
}
func (s *accountStoreStub) ListByOrganization(context.Context, bson.ObjectID, *storage.AccountsFilter, int, int) ([]*pmodel.LedgerAccount, error) {
return nil, nil
return []*pmodel.LedgerAccount{}, nil
}
func (s *accountStoreStub) UpdateStatus(context.Context, bson.ObjectID, pmodel.LedgerAccountStatus) error {

View File

@@ -619,16 +619,27 @@ func parseLedgerAccountType(reader params.Reader, key string) (ledgerv1.AccountT
case string:
return parseLedgerAccountTypeString(v)
case float64:
return ledgerv1.AccountType(int32(v)), nil
truncated := int64(v)
if v != float64(truncated) {
return ledgerv1.AccountType_ACCOUNT_TYPE_UNSPECIFIED, merrors.InvalidArgument("open_account: invalid account_type")
}
return parseLedgerAccountTypeInt64(truncated)
case int:
return ledgerv1.AccountType(v), nil
return parseLedgerAccountTypeInt64(int64(v))
case int64:
return ledgerv1.AccountType(v), nil
return parseLedgerAccountTypeInt64(v)
default:
return ledgerv1.AccountType_ACCOUNT_TYPE_UNSPECIFIED, merrors.InvalidArgument("open_account: account_type is required")
}
}
func parseLedgerAccountTypeInt64(value int64) (ledgerv1.AccountType, error) {
if value < -1<<31 || value > 1<<31-1 {
return ledgerv1.AccountType_ACCOUNT_TYPE_UNSPECIFIED, merrors.InvalidArgument("open_account: invalid account_type")
}
return ledgerv1.AccountType(int32(value)), nil
}
func parseLedgerAccountTypeString(value string) (ledgerv1.AccountType, error) {
accountType, ok := ledgerconv.ParseAccountType(value)
if !ok || ledgerconv.IsAccountTypeUnspecified(value) {
@@ -662,7 +673,7 @@ func parseEventTime(reader params.Reader) *timestamppb.Timestamp {
func parseLedgerCharges(reader params.Reader) ([]*ledgerv1.PostingLine, error) {
items := reader.List("charges")
if len(items) == 0 {
return nil, nil
return nil, nil //nolint:nilnil // nil charges means no extra posting lines were provided
}
result := make([]*ledgerv1.PostingLine, 0, len(items))
for i, item := range items {

View File

@@ -448,7 +448,7 @@ func TestExternalInvariantRandomSequence(t *testing.T) {
require.NoError(t, repo.accounts.Create(ctx, pending))
require.NoError(t, repo.accounts.Create(ctx, transit))
rng := rand.New(rand.NewSource(42))
rng := rand.New(rand.NewSource(42)) //nolint:gosec // deterministic pseudo-random sequence for invariant stress test
for i := 0; i < 50; i++ {
switch rng.Intn(3) {
case 0:

View File

@@ -124,7 +124,9 @@ func (s *Service) listOrganizationAccounts(ctx context.Context, currency string)
if err != nil {
return nil, err
}
defer cursor.Close(ctx)
defer func() {
_ = cursor.Close(ctx)
}()
accounts := make([]*pmodel.LedgerAccount, 0)
for cursor.Next(ctx) {

View File

@@ -12,8 +12,7 @@ import (
me "github.com/tech/sendico/pkg/messaging/envelope"
pmessagingreliable "github.com/tech/sendico/pkg/messaging/reliable"
"github.com/tech/sendico/pkg/mlogger"
cfgmodel "github.com/tech/sendico/pkg/model"
domainmodel "github.com/tech/sendico/pkg/model"
pmodel "github.com/tech/sendico/pkg/model"
notification "github.com/tech/sendico/pkg/model/notification"
"github.com/tech/sendico/pkg/mservice"
)
@@ -35,7 +34,7 @@ type ledgerOutboxStoreAdapter struct {
store storage.OutboxStore
}
func newLedgerReliableProducer(logger mlogger.Logger, direct pmessaging.Producer, store storage.OutboxStore, messagingSettings cfgmodel.SettingsT) (*pmessagingreliable.ReliableProducer, pmessagingreliable.Settings, error) {
func newLedgerReliableProducer(logger mlogger.Logger, direct pmessaging.Producer, store storage.OutboxStore, messagingSettings pmodel.SettingsT) (*pmessagingreliable.ReliableProducer, pmessagingreliable.Settings, error) {
if store == nil {
return nil, pmessagingreliable.DefaultSettings(), nil
}
@@ -71,7 +70,7 @@ func (a *ledgerOutboxStoreAdapter) Enqueue(ctx context.Context, msg pmessagingre
func (a *ledgerOutboxStoreAdapter) ListPending(ctx context.Context, limit int) ([]pmessagingreliable.OutboxMessage, error) {
if a == nil || a.store == nil {
return nil, nil
return nil, nil //nolint:nilnil // nil adapter/store means no pending outbox messages
}
events, err := a.store.ListPending(ctx, limit)
if err != nil {
@@ -160,7 +159,7 @@ func buildLedgerOutboxEnvelope(eventID string, payload []byte, attempts int, org
return nil, err
}
env := me.CreateEnvelope(outboxPublisherSender, domainmodel.NewNotification(mservice.LedgerOutbox, notification.NASent))
env := me.CreateEnvelope(outboxPublisherSender, pmodel.NewNotification(mservice.LedgerOutbox, notification.NASent))
if _, err = env.Wrap(body); err != nil {
return nil, err
}

View File

@@ -2,6 +2,7 @@ package ledger
import (
"context"
"errors"
"fmt"
"strings"
"time"
@@ -73,7 +74,7 @@ func (s *Service) postCreditResponder(_ context.Context, req *ledgerv1.PostCredi
EntryType: ledgerv1.EntryType_ENTRY_CREDIT,
}, nil
}
if err != nil && err != storage.ErrJournalEntryNotFound {
if err != nil && !errors.Is(err, storage.ErrJournalEntryNotFound) {
recordJournalEntryError(journalEntryTypeCredit, journalEntryErrorIdempotencyCheck)
logger.Warn("Failed to check idempotency", zap.Error(err))
return nil, merrors.Internal("failed to check idempotency")
@@ -122,7 +123,7 @@ func (s *Service) postCreditResponder(_ context.Context, req *ledgerv1.PostCredi
chargeAccount, err := s.getAccount(ctx, accountsByRef, chargeAccountRef)
if err != nil {
if err == storage.ErrAccountNotFound {
if errors.Is(err, storage.ErrAccountNotFound) {
return nil, merrors.NoData(fmt.Sprintf("charges[%d]: account not found", i))
}
logger.Warn("Failed to get charge account", zap.Error(err), zap.String("chargeAccountRef", chargeAccountRef.Hex()))

View File

@@ -2,6 +2,7 @@ package ledger
import (
"context"
"errors"
"fmt"
"strings"
"time"
@@ -71,7 +72,7 @@ func (s *Service) postDebitResponder(_ context.Context, req *ledgerv1.PostDebitR
EntryType: ledgerv1.EntryType_ENTRY_DEBIT,
}, nil
}
if err != nil && err != storage.ErrJournalEntryNotFound {
if err != nil && !errors.Is(err, storage.ErrJournalEntryNotFound) {
logger.Warn("Failed to check idempotency", zap.Error(err))
return nil, merrors.Internal("failed to check idempotency")
}
@@ -119,7 +120,7 @@ func (s *Service) postDebitResponder(_ context.Context, req *ledgerv1.PostDebitR
chargeAccount, err := s.getAccount(ctx, accountsByRef, chargeAccountRef)
if err != nil {
if err == storage.ErrAccountNotFound {
if errors.Is(err, storage.ErrAccountNotFound) {
return nil, merrors.NoData(fmt.Sprintf("charges[%d]: account not found", i))
}
logger.Warn("Failed to get charge account", zap.Error(err), mzap.ObjRef("charge_account_ref", chargeAccountRef))

View File

@@ -2,6 +2,7 @@ package ledger
import (
"context"
"errors"
"fmt"
"strings"
"time"
@@ -69,7 +70,7 @@ func (s *Service) postExternalCreditResponder(_ context.Context, req *ledgerv1.P
EntryType: ledgerv1.EntryType_ENTRY_CREDIT,
}, nil
}
if err != nil && err != storage.ErrJournalEntryNotFound {
if err != nil && !errors.Is(err, storage.ErrJournalEntryNotFound) {
recordJournalEntryError(journalEntryTypeCredit, journalEntryErrorIdempotencyCheck)
logger.Warn("Failed to check idempotency", zap.Error(err))
return nil, merrors.Internal("failed to check idempotency")
@@ -137,7 +138,7 @@ func (s *Service) postExternalCreditResponder(_ context.Context, req *ledgerv1.P
chargeAccount, err := s.getAccount(ctx, accountsByRef, chargeAccountRef)
if err != nil {
if err == storage.ErrAccountNotFound {
if errors.Is(err, storage.ErrAccountNotFound) {
return nil, merrors.NoData(fmt.Sprintf("charges[%d]: account not found", i))
}
logger.Warn("Failed to get charge account", zap.Error(err), mzap.ObjRef("charge_account_ref", chargeAccountRef))
@@ -294,7 +295,7 @@ func (s *Service) postExternalDebitResponder(_ context.Context, req *ledgerv1.Po
EntryType: ledgerv1.EntryType_ENTRY_DEBIT,
}, nil
}
if err != nil && err != storage.ErrJournalEntryNotFound {
if err != nil && !errors.Is(err, storage.ErrJournalEntryNotFound) {
recordJournalEntryError(journalEntryTypeDebit, journalEntryErrorIdempotencyCheck)
logger.Warn("Failed to check idempotency", zap.Error(err))
return nil, merrors.Internal("failed to check idempotency")
@@ -362,7 +363,7 @@ func (s *Service) postExternalDebitResponder(_ context.Context, req *ledgerv1.Po
chargeAccount, err := s.getAccount(ctx, accountsByRef, chargeAccountRef)
if err != nil {
if err == storage.ErrAccountNotFound {
if errors.Is(err, storage.ErrAccountNotFound) {
return nil, merrors.NoData(fmt.Sprintf("charges[%d]: account not found", i))
}
logger.Warn("Failed to get charge account", zap.Error(err), mzap.ObjRef("charge_account_ref", chargeAccountRef))

View File

@@ -2,6 +2,7 @@ package ledger
import (
"context"
"errors"
"fmt"
"time"
@@ -84,7 +85,7 @@ func (s *Service) fxResponder(_ context.Context, req *ledgerv1.FXRequest) gsresp
EntryType: ledgerv1.EntryType_ENTRY_FX,
}, nil
}
if err != nil && err != storage.ErrJournalEntryNotFound {
if err != nil && !errors.Is(err, storage.ErrJournalEntryNotFound) {
logger.Warn("Failed to check idempotency", zap.Error(err))
return nil, merrors.Internal("failed to check idempotency")
}
@@ -92,7 +93,7 @@ func (s *Service) fxResponder(_ context.Context, req *ledgerv1.FXRequest) gsresp
// Verify both accounts exist and are active
fromAccount, err := s.storage.Accounts().Get(ctx, fromAccountRef)
if err != nil {
if err == storage.ErrAccountNotFound {
if errors.Is(err, storage.ErrAccountNotFound) {
return nil, merrors.NoData("from_account not found")
}
logger.Warn("Failed to get from_account", zap.Error(err))
@@ -104,7 +105,7 @@ func (s *Service) fxResponder(_ context.Context, req *ledgerv1.FXRequest) gsresp
toAccount, err := s.storage.Accounts().Get(ctx, toAccountRef)
if err != nil {
if err == storage.ErrAccountNotFound {
if errors.Is(err, storage.ErrAccountNotFound) {
return nil, merrors.NoData("to_account not found")
}
logger.Warn("Failed to get to_account", zap.Error(err))
@@ -158,7 +159,7 @@ func (s *Service) fxResponder(_ context.Context, req *ledgerv1.FXRequest) gsresp
chargeAccount, err := s.getAccount(ctx, accountsByRef, chargeAccountRef)
if err != nil {
if err == storage.ErrAccountNotFound {
if errors.Is(err, storage.ErrAccountNotFound) {
return nil, merrors.NoData(fmt.Sprintf("charges[%d]: account not found", i))
}
logger.Warn("Failed to get FX charge account", zap.Error(err), mzap.ObjRef("charge_account_ref", chargeAccountRef))

View File

@@ -2,6 +2,7 @@ package ledger
import (
"context"
"errors"
"fmt"
"strings"
"time"
@@ -94,7 +95,7 @@ func (s *Service) transferResponder(_ context.Context, req *ledgerv1.TransferReq
EntryType: ledgerv1.EntryType_ENTRY_TRANSFER,
}, nil
}
if err != nil && err != storage.ErrJournalEntryNotFound {
if err != nil && !errors.Is(err, storage.ErrJournalEntryNotFound) {
logger.Warn("Failed to check idempotency", zap.Error(err))
return nil, merrors.Internal("failed to check idempotency")
}
@@ -168,7 +169,7 @@ func (s *Service) transferResponder(_ context.Context, req *ledgerv1.TransferReq
chargeAccount, err := s.getAccount(ctx, accountsByRef, chargeAccountRef)
if err != nil {
if err == storage.ErrAccountNotFound {
if errors.Is(err, storage.ErrAccountNotFound) {
return nil, merrors.NoData(fmt.Sprintf("charges[%d]: account not found", i))
}
logger.Warn("Failed to get charge account", zap.Error(err), mzap.ObjRef("charge_account_ref", chargeAccountRef))

View File

@@ -3,6 +3,7 @@ package ledger
import (
"context"
"encoding/base64"
"errors"
"fmt"
"strconv"
"strings"
@@ -33,7 +34,7 @@ func (s *Service) getBalanceResponder(_ context.Context, req *ledgerv1.GetBalanc
// Get account to verify it exists
account, err := s.storage.Accounts().Get(ctx, accountRef)
if err != nil {
if err == storage.ErrAccountNotFound {
if errors.Is(err, storage.ErrAccountNotFound) {
return nil, merrors.NoData("account not found")
}
logger.Warn("Failed to get account", zap.Error(err))
@@ -43,7 +44,7 @@ func (s *Service) getBalanceResponder(_ context.Context, req *ledgerv1.GetBalanc
// Get balance
balance, err := s.storage.Balances().Get(ctx, accountRef)
if err != nil {
if err == storage.ErrBalanceNotFound {
if errors.Is(err, storage.ErrBalanceNotFound) {
// Return zero balance if account exists but has no balance yet
return &ledgerv1.BalanceResponse{
LedgerAccountRef: req.LedgerAccountRef,
@@ -89,7 +90,7 @@ func (s *Service) getJournalEntryResponder(_ context.Context, req *ledgerv1.GetE
// Get journal entry
entry, err := s.storage.JournalEntries().Get(ctx, entryRef)
if err != nil {
if err == storage.ErrJournalEntryNotFound {
if errors.Is(err, storage.ErrJournalEntryNotFound) {
return nil, merrors.NoData("journal entry not found")
}
logger.Warn("Failed to get journal entry", zap.Error(err))
@@ -148,7 +149,7 @@ func (s *Service) getStatementResponder(_ context.Context, req *ledgerv1.GetStat
// Verify account exists
_, err = s.storage.Accounts().Get(ctx, accountRef)
if err != nil {
if err == storage.ErrAccountNotFound {
if errors.Is(err, storage.ErrAccountNotFound) {
return nil, merrors.NoData("account not found")
}
logger.Warn("Failed to get account", zap.Error(err))

View File

@@ -415,7 +415,7 @@ func (s *Service) startDiscoveryAnnouncer() {
InvokeURI: s.invokeURI,
Version: appversion.Create().Short(),
}
s.announcer = discovery.NewAnnouncer(s.logger, s.producer, string(mservice.Ledger), announce)
s.announcer = discovery.NewAnnouncer(s.logger, s.producer, mservice.Ledger, announce)
s.announcer.Start()
}
@@ -446,7 +446,7 @@ func (s *Service) startOutboxReliableProducer() error {
zap.Int("poll_interval_seconds", settings.PollIntervalSeconds),
zap.Int("max_attempts", settings.MaxAttempts))
ctx, cancel := context.WithCancel(context.Background())
ctx, cancel := context.WithCancel(context.Background()) //nolint:gosec // canceled explicitly in Shutdown
s.outbox.cancel = cancel
go s.outbox.producer.Run(ctx)
})