ledger accounts improvement

This commit is contained in:
Stephan D
2026-01-30 15:54:45 +01:00
parent 51f5b0804a
commit 17dde423f6
40 changed files with 3355 additions and 570 deletions

View File

@@ -9,6 +9,7 @@ import (
"github.com/tech/sendico/pkg/ledgerconv"
"github.com/tech/sendico/pkg/merrors"
"github.com/tech/sendico/pkg/model"
"github.com/tech/sendico/pkg/payments/rail"
describablev1 "github.com/tech/sendico/pkg/proto/common/describable/v1"
moneyv1 "github.com/tech/sendico/pkg/proto/common/money/v1"
@@ -38,6 +39,9 @@ type Client interface {
TransferInternal(ctx context.Context, req *ledgerv1.TransferRequest) (*ledgerv1.PostResponse, error)
ApplyFXWithCharges(ctx context.Context, req *ledgerv1.FXRequest) (*ledgerv1.PostResponse, error)
BlockAccount(ctx context.Context, req *ledgerv1.BlockAccountRequest) (*ledgerv1.BlockAccountResponse, error)
UnblockAccount(ctx context.Context, req *ledgerv1.UnblockAccountRequest) (*ledgerv1.UnblockAccountResponse, error)
GetBalance(ctx context.Context, req *ledgerv1.GetBalanceRequest) (*ledgerv1.BalanceResponse, error)
GetJournalEntry(ctx context.Context, req *ledgerv1.GetEntryRequest) (*ledgerv1.JournalEntryResponse, error)
GetStatement(ctx context.Context, req *ledgerv1.GetStatementRequest) (*ledgerv1.StatementResponse, error)
@@ -50,6 +54,7 @@ type grpcConnectorClient interface {
GetAccount(ctx context.Context, in *connectorv1.GetAccountRequest, opts ...grpc.CallOption) (*connectorv1.GetAccountResponse, error)
ListAccounts(ctx context.Context, in *connectorv1.ListAccountsRequest, opts ...grpc.CallOption) (*connectorv1.ListAccountsResponse, error)
GetBalance(ctx context.Context, in *connectorv1.GetBalanceRequest, opts ...grpc.CallOption) (*connectorv1.GetBalanceResponse, error)
UpdateAccountState(ctx context.Context, in *connectorv1.UpdateAccountStateRequest, opts ...grpc.CallOption) (*connectorv1.UpdateAccountStateResponse, error)
SubmitOperation(ctx context.Context, in *connectorv1.SubmitOperationRequest, opts ...grpc.CallOption) (*connectorv1.SubmitOperationResponse, error)
GetOperation(ctx context.Context, in *connectorv1.GetOperationRequest, opts ...grpc.CallOption) (*connectorv1.GetOperationResponse, error)
ListOperations(ctx context.Context, in *connectorv1.ListOperationsRequest, opts ...grpc.CallOption) (*connectorv1.ListOperationsResponse, error)
@@ -141,10 +146,17 @@ func (c *ledgerClient) CreateTransaction(ctx context.Context, tx rail.LedgerTx)
description := strings.TrimSpace(tx.Description)
metadata := ledgerTxMetadata(tx.Metadata, tx)
extraParams := map[string]interface{}{}
if op := strings.TrimSpace(tx.Operation); op != "" {
extraParams["operation"] = op
}
if len(extraParams) == 0 {
extraParams = nil
}
switch {
case isLedgerRail(tx.FromRail) && !isLedgerRail(tx.ToRail):
resp, err := c.PostDebitWithCharges(ctx, &ledgerv1.PostDebitRequest{
resp, err := c.submitLedgerOperationWithExtras(ctx, connectorv1.OperationType_DEBIT, accountRef, "", money, &ledgerv1.PostDebitRequest{
IdempotencyKey: strings.TrimSpace(tx.IdempotencyKey),
OrganizationRef: orgRef,
LedgerAccountRef: accountRef,
@@ -153,13 +165,13 @@ func (c *ledgerClient) CreateTransaction(ctx context.Context, tx rail.LedgerTx)
Charges: tx.Charges,
Metadata: metadata,
ContraLedgerAccountRef: strings.TrimSpace(tx.ContraLedgerAccountRef),
})
}, extraParams)
if err != nil {
return "", err
}
return strings.TrimSpace(resp.GetJournalEntryRef()), nil
case isLedgerRail(tx.ToRail) && !isLedgerRail(tx.FromRail):
resp, err := c.PostCreditWithCharges(ctx, &ledgerv1.PostCreditRequest{
resp, err := c.submitLedgerOperationWithExtras(ctx, connectorv1.OperationType_CREDIT, "", accountRef, money, &ledgerv1.PostCreditRequest{
IdempotencyKey: strings.TrimSpace(tx.IdempotencyKey),
OrganizationRef: orgRef,
LedgerAccountRef: accountRef,
@@ -168,7 +180,7 @@ func (c *ledgerClient) CreateTransaction(ctx context.Context, tx rail.LedgerTx)
Charges: tx.Charges,
Metadata: metadata,
ContraLedgerAccountRef: strings.TrimSpace(tx.ContraLedgerAccountRef),
})
}, extraParams)
if err != nil {
return "", err
}
@@ -196,7 +208,9 @@ func (c *ledgerClient) CreateAccount(ctx context.Context, req *ledgerv1.CreateAc
"account_type": req.GetAccountType().String(),
"status": req.GetStatus().String(),
"allow_negative": req.GetAllowNegative(),
"is_settlement": req.GetIsSettlement(),
}
if role := req.GetRole(); role != ledgerv1.AccountRole_ACCOUNT_ROLE_UNSPECIFIED {
params["role"] = role.String()
}
label := ""
if desc := req.GetDescribable(); desc != nil {
@@ -232,7 +246,10 @@ func (c *ledgerClient) ListAccounts(ctx context.Context, req *ledgerv1.ListAccou
if req == nil || strings.TrimSpace(req.GetOrganizationRef()) == "" {
return nil, merrors.InvalidArgument("ledger: organization_ref is required")
}
resp, err := c.client.ListAccounts(ctx, &connectorv1.ListAccountsRequest{OrganizationRef: strings.TrimSpace(req.GetOrganizationRef())})
resp, err := c.client.ListAccounts(ctx, &connectorv1.ListAccountsRequest{
OrganizationRef: strings.TrimSpace(req.GetOrganizationRef()),
OwnerRefFilter: req.GetOwnerRefFilter(),
})
if err != nil {
return nil, err
}
@@ -294,6 +311,48 @@ func (c *ledgerClient) ApplyFXWithCharges(ctx context.Context, req *ledgerv1.FXR
return &ledgerv1.PostResponse{JournalEntryRef: resp.GetReceipt().GetOperationId(), EntryType: ledgerv1.EntryType_ENTRY_FX}, nil
}
func (c *ledgerClient) BlockAccount(ctx context.Context, req *ledgerv1.BlockAccountRequest) (*ledgerv1.BlockAccountResponse, error) {
ctx, cancel := c.callContext(ctx)
defer cancel()
if req == nil || strings.TrimSpace(req.GetLedgerAccountRef()) == "" {
return nil, merrors.InvalidArgument("ledger: ledger_account_ref is required")
}
sourceRole := model.ToProto(accountRoleFromLedgerProto(req.GetRole()))
resp, err := c.client.UpdateAccountState(ctx, &connectorv1.UpdateAccountStateRequest{
AccountRef: &connectorv1.AccountRef{ConnectorId: ledgerConnectorID, AccountId: strings.TrimSpace(req.GetLedgerAccountRef())},
TargetState: connectorv1.AccountState_ACCOUNT_SUSPENDED,
SourceRole: sourceRole,
})
if err != nil {
return nil, err
}
if resp.GetError() != nil {
return nil, connectorError(resp.GetError())
}
return &ledgerv1.BlockAccountResponse{Account: ledgerAccountFromConnector(resp.GetAccount())}, nil
}
func (c *ledgerClient) UnblockAccount(ctx context.Context, req *ledgerv1.UnblockAccountRequest) (*ledgerv1.UnblockAccountResponse, error) {
ctx, cancel := c.callContext(ctx)
defer cancel()
if req == nil || strings.TrimSpace(req.GetLedgerAccountRef()) == "" {
return nil, merrors.InvalidArgument("ledger: ledger_account_ref is required")
}
sourceRole := model.ToProto(accountRoleFromLedgerProto(req.GetRole()))
resp, err := c.client.UpdateAccountState(ctx, &connectorv1.UpdateAccountStateRequest{
AccountRef: &connectorv1.AccountRef{ConnectorId: ledgerConnectorID, AccountId: strings.TrimSpace(req.GetLedgerAccountRef())},
TargetState: connectorv1.AccountState_ACCOUNT_ACTIVE,
SourceRole: sourceRole,
})
if err != nil {
return nil, err
}
if resp.GetError() != nil {
return nil, connectorError(resp.GetError())
}
return &ledgerv1.UnblockAccountResponse{Account: ledgerAccountFromConnector(resp.GetAccount())}, nil
}
func (c *ledgerClient) GetBalance(ctx context.Context, req *ledgerv1.GetBalanceRequest) (*ledgerv1.BalanceResponse, error) {
ctx, cancel := c.callContext(ctx)
defer cancel()
@@ -353,6 +412,10 @@ func (c *ledgerClient) GetStatement(ctx context.Context, req *ledgerv1.GetStatem
}
func (c *ledgerClient) submitLedgerOperation(ctx context.Context, opType connectorv1.OperationType, fromRef, toRef string, money *moneyv1.Money, req interface{}) (*ledgerv1.PostResponse, error) {
return c.submitLedgerOperationWithExtras(ctx, opType, fromRef, toRef, money, req, nil)
}
func (c *ledgerClient) submitLedgerOperationWithExtras(ctx context.Context, opType connectorv1.OperationType, fromRef, toRef string, money *moneyv1.Money, req interface{}, extraParams map[string]interface{}) (*ledgerv1.PostResponse, error) {
ctx, cancel := c.callContext(ctx)
defer cancel()
if money == nil {
@@ -367,6 +430,8 @@ func (c *ledgerClient) submitLedgerOperation(ctx context.Context, opType connect
charges []*ledgerv1.PostingLine
eventTime *timestamppb.Timestamp
contraRef string
fromRole model.AccountRole
toRole model.AccountRole
)
switch r := req.(type) {
@@ -378,6 +443,7 @@ func (c *ledgerClient) submitLedgerOperation(ctx context.Context, opType connect
charges = r.GetCharges()
eventTime = r.GetEventTime()
contraRef = r.GetContraLedgerAccountRef()
toRole = accountRoleFromLedgerProto(r.GetRole())
case *ledgerv1.PostDebitRequest:
idempotencyKey = r.GetIdempotencyKey()
orgRef = r.GetOrganizationRef()
@@ -386,6 +452,7 @@ func (c *ledgerClient) submitLedgerOperation(ctx context.Context, opType connect
charges = r.GetCharges()
eventTime = r.GetEventTime()
contraRef = r.GetContraLedgerAccountRef()
fromRole = accountRoleFromLedgerProto(r.GetRole())
case *ledgerv1.TransferRequest:
idempotencyKey = r.GetIdempotencyKey()
orgRef = r.GetOrganizationRef()
@@ -393,12 +460,19 @@ func (c *ledgerClient) submitLedgerOperation(ctx context.Context, opType connect
metadata = r.GetMetadata()
charges = r.GetCharges()
eventTime = r.GetEventTime()
fromRole = accountRoleFromLedgerProto(r.GetFromRole())
toRole = accountRoleFromLedgerProto(r.GetToRole())
}
params := ledgerOperationParams(orgRef, description, metadata, charges, eventTime)
if contraRef != "" {
params["contra_ledger_account_ref"] = strings.TrimSpace(contraRef)
}
if len(extraParams) > 0 {
for key, value := range extraParams {
params[key] = value
}
}
op := &connectorv1.Operation{
Type: opType,
@@ -412,6 +486,12 @@ func (c *ledgerClient) submitLedgerOperation(ctx context.Context, opType connect
if toRef != "" {
op.To = accountParty(toRef)
}
if fromRole != "" {
op.FromRole = model.ToProto(fromRole)
}
if toRole != "" {
op.ToRole = model.ToProto(toRole)
}
resp, err := c.client.SubmitOperation(ctx, &connectorv1.SubmitOperationRequest{Operation: op})
if err != nil {
@@ -423,6 +503,35 @@ func (c *ledgerClient) submitLedgerOperation(ctx context.Context, opType connect
return &ledgerv1.PostResponse{JournalEntryRef: resp.GetReceipt().GetOperationId(), EntryType: entryTypeFromOperation(opType)}, nil
}
func accountRoleFromLedgerProto(role ledgerv1.AccountRole) model.AccountRole {
switch role {
case ledgerv1.AccountRole_ACCOUNT_ROLE_OPERATING:
return model.AccountRoleOperating
case ledgerv1.AccountRole_ACCOUNT_ROLE_HOLD:
return model.AccountRoleHold
case ledgerv1.AccountRole_ACCOUNT_ROLE_TRANSIT:
return model.AccountRoleTransit
case ledgerv1.AccountRole_ACCOUNT_ROLE_SETTLEMENT:
return model.AccountRoleSettlement
case ledgerv1.AccountRole_ACCOUNT_ROLE_CLEARING:
return model.AccountRoleClearing
case ledgerv1.AccountRole_ACCOUNT_ROLE_PENDING:
return model.AccountRolePending
case ledgerv1.AccountRole_ACCOUNT_ROLE_RESERVE:
return model.AccountRoleReserve
case ledgerv1.AccountRole_ACCOUNT_ROLE_LIQUIDITY:
return model.AccountRoleLiquidity
case ledgerv1.AccountRole_ACCOUNT_ROLE_FEE:
return model.AccountRoleFee
case ledgerv1.AccountRole_ACCOUNT_ROLE_CHARGEBACK:
return model.AccountRoleChargeback
case ledgerv1.AccountRole_ACCOUNT_ROLE_ADJUSTMENT:
return model.AccountRoleAdjustment
default:
return ""
}
}
func ledgerOperationParams(orgRef, description string, metadata map[string]string, charges []*ledgerv1.PostingLine, eventTime *timestamppb.Timestamp) map[string]interface{} {
params := map[string]interface{}{
"organization_ref": strings.TrimSpace(orgRef),
@@ -482,9 +591,23 @@ func ledgerAccountFromConnector(account *connectorv1.Account) *ledgerv1.LedgerAc
if v, ok := details["allow_negative"].(bool); ok {
allowNegative = v
}
isSettlement := false
if v, ok := details["is_settlement"].(bool); ok {
isSettlement = v
role := ledgerv1.AccountRole_ACCOUNT_ROLE_UNSPECIFIED
if v := strings.TrimSpace(fmt.Sprint(details["role"])); v != "" {
if parsed, ok := ledgerconv.ParseAccountRole(v); ok {
role = parsed
}
}
if role == ledgerv1.AccountRole_ACCOUNT_ROLE_UNSPECIFIED {
switch v := details["is_settlement"].(type) {
case bool:
if v {
role = ledgerv1.AccountRole_ACCOUNT_ROLE_SETTLEMENT
}
case string:
if strings.EqualFold(strings.TrimSpace(v), "true") {
role = ledgerv1.AccountRole_ACCOUNT_ROLE_SETTLEMENT
}
}
}
accountCode := strings.TrimSpace(fmt.Sprint(details["account_code"]))
accountID := ""
@@ -515,7 +638,7 @@ func ledgerAccountFromConnector(account *connectorv1.Account) *ledgerv1.LedgerAc
Currency: strings.TrimSpace(account.GetAsset()),
Status: status,
AllowNegative: allowNegative,
IsSettlement: isSettlement,
Role: role,
CreatedAt: account.GetCreatedAt(),
UpdatedAt: account.GetUpdatedAt(),
Describable: describable,

View File

@@ -32,6 +32,10 @@ func (s *stubConnector) GetBalance(context.Context, *connectorv1.GetBalanceReque
return nil, nil
}
func (s *stubConnector) UpdateAccountState(context.Context, *connectorv1.UpdateAccountStateRequest, ...grpc.CallOption) (*connectorv1.UpdateAccountStateResponse, error) {
return nil, nil
}
func (s *stubConnector) SubmitOperation(ctx context.Context, req *connectorv1.SubmitOperationRequest, _ ...grpc.CallOption) (*connectorv1.SubmitOperationResponse, error) {
if s.submitFn != nil {
return s.submitFn(ctx, req)

View File

@@ -21,6 +21,8 @@ type Fake struct {
PostDebitWithChargesFn func(ctx context.Context, req *ledgerv1.PostDebitRequest) (*ledgerv1.PostResponse, error)
TransferInternalFn func(ctx context.Context, req *ledgerv1.TransferRequest) (*ledgerv1.PostResponse, error)
ApplyFXWithChargesFn func(ctx context.Context, req *ledgerv1.FXRequest) (*ledgerv1.PostResponse, error)
BlockAccountFn func(ctx context.Context, req *ledgerv1.BlockAccountRequest) (*ledgerv1.BlockAccountResponse, error)
UnblockAccountFn func(ctx context.Context, req *ledgerv1.UnblockAccountRequest) (*ledgerv1.UnblockAccountResponse, error)
GetBalanceFn func(ctx context.Context, req *ledgerv1.GetBalanceRequest) (*ledgerv1.BalanceResponse, error)
GetJournalEntryFn func(ctx context.Context, req *ledgerv1.GetEntryRequest) (*ledgerv1.JournalEntryResponse, error)
GetStatementFn func(ctx context.Context, req *ledgerv1.GetStatementRequest) (*ledgerv1.StatementResponse, error)
@@ -97,6 +99,20 @@ func (f *Fake) ApplyFXWithCharges(ctx context.Context, req *ledgerv1.FXRequest)
return &ledgerv1.PostResponse{}, nil
}
func (f *Fake) BlockAccount(ctx context.Context, req *ledgerv1.BlockAccountRequest) (*ledgerv1.BlockAccountResponse, error) {
if f.BlockAccountFn != nil {
return f.BlockAccountFn(ctx, req)
}
return &ledgerv1.BlockAccountResponse{}, nil
}
func (f *Fake) UnblockAccount(ctx context.Context, req *ledgerv1.UnblockAccountRequest) (*ledgerv1.UnblockAccountResponse, error) {
if f.UnblockAccountFn != nil {
return f.UnblockAccountFn(ctx, req)
}
return &ledgerv1.UnblockAccountResponse{}, nil
}
func (f *Fake) GetBalance(ctx context.Context, req *ledgerv1.GetBalanceRequest) (*ledgerv1.BalanceResponse, error) {
if f.GetBalanceFn != nil {
return f.GetBalanceFn(ctx, req)