service backend
This commit is contained in:
166
api/ledger/internal/service/ledger/helpers.go
Normal file
166
api/ledger/internal/service/ledger/helpers.go
Normal file
@@ -0,0 +1,166 @@
|
||||
package ledger
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
ledgerv1 "github.com/tech/sendico/ledger/internal/generated/service/ledger/v1"
|
||||
"github.com/tech/sendico/ledger/storage/model"
|
||||
"github.com/tech/sendico/pkg/merrors"
|
||||
moneyv1 "github.com/tech/sendico/pkg/proto/common/money/v1"
|
||||
"github.com/shopspring/decimal"
|
||||
"go.mongodb.org/mongo-driver/bson/primitive"
|
||||
"google.golang.org/protobuf/types/known/timestamppb"
|
||||
)
|
||||
|
||||
// parseObjectID converts a hex string to ObjectID
|
||||
func parseObjectID(hexID string) (primitive.ObjectID, error) {
|
||||
if hexID == "" {
|
||||
return primitive.NilObjectID, merrors.InvalidArgument("empty object ID")
|
||||
}
|
||||
oid, err := primitive.ObjectIDFromHex(hexID)
|
||||
if err != nil {
|
||||
return primitive.NilObjectID, merrors.InvalidArgument(fmt.Sprintf("invalid object ID: %v", err))
|
||||
}
|
||||
return oid, nil
|
||||
}
|
||||
|
||||
// parseDecimal converts a string amount to decimal
|
||||
func parseDecimal(amount string) (decimal.Decimal, error) {
|
||||
if amount == "" {
|
||||
return decimal.Zero, merrors.InvalidArgument("empty amount")
|
||||
}
|
||||
dec, err := decimal.NewFromString(amount)
|
||||
if err != nil {
|
||||
return decimal.Zero, merrors.InvalidArgument(fmt.Sprintf("invalid decimal amount: %v", err))
|
||||
}
|
||||
return dec, nil
|
||||
}
|
||||
|
||||
// validateMoney checks that a Money message is valid
|
||||
func validateMoney(m *moneyv1.Money, fieldName string) error {
|
||||
if m == nil {
|
||||
return merrors.InvalidArgument(fmt.Sprintf("%s: money is required", fieldName))
|
||||
}
|
||||
if m.Amount == "" {
|
||||
return merrors.InvalidArgument(fmt.Sprintf("%s: amount is required", fieldName))
|
||||
}
|
||||
if m.Currency == "" {
|
||||
return merrors.InvalidArgument(fmt.Sprintf("%s: currency is required", fieldName))
|
||||
}
|
||||
// Validate it's a valid decimal
|
||||
if _, err := parseDecimal(m.Amount); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// validatePostingLines validates charge lines
|
||||
func validatePostingLines(lines []*ledgerv1.PostingLine) error {
|
||||
for i, line := range lines {
|
||||
if line == nil {
|
||||
return merrors.InvalidArgument(fmt.Sprintf("charges[%d]: nil posting line", i))
|
||||
}
|
||||
if line.LedgerAccountRef == "" {
|
||||
return merrors.InvalidArgument(fmt.Sprintf("charges[%d]: ledger_account_ref is required", i))
|
||||
}
|
||||
if line.Money == nil {
|
||||
return merrors.InvalidArgument(fmt.Sprintf("charges[%d]: money is required", i))
|
||||
}
|
||||
if err := validateMoney(line.Money, fmt.Sprintf("charges[%d].money", i)); err != nil {
|
||||
return err
|
||||
}
|
||||
// Charges should not be MAIN type
|
||||
if line.LineType == ledgerv1.LineType_LINE_MAIN {
|
||||
return merrors.InvalidArgument(fmt.Sprintf("charges[%d]: cannot have LINE_MAIN type", i))
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// getEventTime extracts event time from proto or defaults to now
|
||||
func getEventTime(ts *timestamppb.Timestamp) time.Time {
|
||||
if ts != nil && ts.IsValid() {
|
||||
return ts.AsTime()
|
||||
}
|
||||
return time.Now().UTC()
|
||||
}
|
||||
|
||||
// protoLineTypeToModel converts proto LineType to model LineType
|
||||
func protoLineTypeToModel(lt ledgerv1.LineType) model.LineType {
|
||||
switch lt {
|
||||
case ledgerv1.LineType_LINE_MAIN:
|
||||
return model.LineTypeMain
|
||||
case ledgerv1.LineType_LINE_FEE:
|
||||
return model.LineTypeFee
|
||||
case ledgerv1.LineType_LINE_SPREAD:
|
||||
return model.LineTypeSpread
|
||||
case ledgerv1.LineType_LINE_REVERSAL:
|
||||
return model.LineTypeReversal
|
||||
default:
|
||||
return model.LineTypeMain
|
||||
}
|
||||
}
|
||||
|
||||
// modelLineTypeToProto converts model LineType to proto LineType
|
||||
func modelLineTypeToProto(lt model.LineType) ledgerv1.LineType {
|
||||
switch lt {
|
||||
case model.LineTypeMain:
|
||||
return ledgerv1.LineType_LINE_MAIN
|
||||
case model.LineTypeFee:
|
||||
return ledgerv1.LineType_LINE_FEE
|
||||
case model.LineTypeSpread:
|
||||
return ledgerv1.LineType_LINE_SPREAD
|
||||
case model.LineTypeReversal:
|
||||
return ledgerv1.LineType_LINE_REVERSAL
|
||||
default:
|
||||
return ledgerv1.LineType_LINE_TYPE_UNSPECIFIED
|
||||
}
|
||||
}
|
||||
|
||||
// modelEntryTypeToProto converts model EntryType to proto EntryType
|
||||
func modelEntryTypeToProto(et model.EntryType) ledgerv1.EntryType {
|
||||
switch et {
|
||||
case model.EntryTypeCredit:
|
||||
return ledgerv1.EntryType_ENTRY_CREDIT
|
||||
case model.EntryTypeDebit:
|
||||
return ledgerv1.EntryType_ENTRY_DEBIT
|
||||
case model.EntryTypeTransfer:
|
||||
return ledgerv1.EntryType_ENTRY_TRANSFER
|
||||
case model.EntryTypeFX:
|
||||
return ledgerv1.EntryType_ENTRY_FX
|
||||
case model.EntryTypeFee:
|
||||
return ledgerv1.EntryType_ENTRY_FEE
|
||||
case model.EntryTypeAdjust:
|
||||
return ledgerv1.EntryType_ENTRY_ADJUST
|
||||
case model.EntryTypeReverse:
|
||||
return ledgerv1.EntryType_ENTRY_REVERSE
|
||||
default:
|
||||
return ledgerv1.EntryType_ENTRY_TYPE_UNSPECIFIED
|
||||
}
|
||||
}
|
||||
|
||||
// calculateBalance computes net balance from a set of posting lines
|
||||
func calculateBalance(lines []*model.PostingLine) (decimal.Decimal, error) {
|
||||
balance := decimal.Zero
|
||||
for _, line := range lines {
|
||||
amount, err := parseDecimal(line.Amount)
|
||||
if err != nil {
|
||||
return decimal.Zero, fmt.Errorf("invalid line amount: %w", err)
|
||||
}
|
||||
balance = balance.Add(amount)
|
||||
}
|
||||
return balance, nil
|
||||
}
|
||||
|
||||
// validateBalanced ensures posting lines sum to zero (double-entry accounting)
|
||||
func validateBalanced(lines []*model.PostingLine) error {
|
||||
balance, err := calculateBalance(lines)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !balance.IsZero() {
|
||||
return merrors.InvalidArgument(fmt.Sprintf("journal entry must balance (sum=0), got: %s", balance.String()))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user