702 lines
25 KiB
Go
702 lines
25 KiB
Go
package client
|
|
|
|
import (
|
|
"context"
|
|
"crypto/tls"
|
|
"fmt"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/tech/sendico/pkg/merrors"
|
|
"github.com/tech/sendico/pkg/payments/rail"
|
|
connectorv1 "github.com/tech/sendico/pkg/proto/connector/v1"
|
|
moneyv1 "github.com/tech/sendico/pkg/proto/common/money/v1"
|
|
paginationv1 "github.com/tech/sendico/pkg/proto/common/pagination/v1"
|
|
ledgerv1 "github.com/tech/sendico/pkg/proto/ledger/v1"
|
|
"google.golang.org/grpc"
|
|
"google.golang.org/grpc/credentials"
|
|
"google.golang.org/grpc/credentials/insecure"
|
|
"google.golang.org/protobuf/types/known/structpb"
|
|
"google.golang.org/protobuf/types/known/timestamppb"
|
|
)
|
|
|
|
const ledgerConnectorID = "ledger"
|
|
|
|
// Client exposes typed helpers around the ledger gRPC API.
|
|
type Client interface {
|
|
ReadBalance(ctx context.Context, accountID string) (*moneyv1.Money, error)
|
|
CreateTransaction(ctx context.Context, tx rail.LedgerTx) (string, error)
|
|
HoldBalance(ctx context.Context, accountID string, amount string) error
|
|
|
|
CreateAccount(ctx context.Context, req *ledgerv1.CreateAccountRequest) (*ledgerv1.CreateAccountResponse, error)
|
|
ListAccounts(ctx context.Context, req *ledgerv1.ListAccountsRequest) (*ledgerv1.ListAccountsResponse, error)
|
|
PostCreditWithCharges(ctx context.Context, req *ledgerv1.PostCreditRequest) (*ledgerv1.PostResponse, error)
|
|
PostDebitWithCharges(ctx context.Context, req *ledgerv1.PostDebitRequest) (*ledgerv1.PostResponse, error)
|
|
TransferInternal(ctx context.Context, req *ledgerv1.TransferRequest) (*ledgerv1.PostResponse, error)
|
|
ApplyFXWithCharges(ctx context.Context, req *ledgerv1.FXRequest) (*ledgerv1.PostResponse, 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)
|
|
|
|
Close() error
|
|
}
|
|
|
|
type grpcConnectorClient interface {
|
|
OpenAccount(ctx context.Context, in *connectorv1.OpenAccountRequest, opts ...grpc.CallOption) (*connectorv1.OpenAccountResponse, error)
|
|
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)
|
|
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)
|
|
}
|
|
|
|
type ledgerClient struct {
|
|
cfg Config
|
|
conn *grpc.ClientConn
|
|
client grpcConnectorClient
|
|
}
|
|
|
|
// New dials the ledger endpoint and returns a ready client.
|
|
func New(ctx context.Context, cfg Config, opts ...grpc.DialOption) (Client, error) {
|
|
cfg.setDefaults()
|
|
if strings.TrimSpace(cfg.Address) == "" {
|
|
return nil, merrors.InvalidArgument("ledger: address is required")
|
|
}
|
|
|
|
dialCtx, cancel := context.WithTimeout(ctx, cfg.DialTimeout)
|
|
defer cancel()
|
|
|
|
dialOpts := make([]grpc.DialOption, 0, len(opts)+1)
|
|
dialOpts = append(dialOpts, opts...)
|
|
|
|
if cfg.Insecure {
|
|
dialOpts = append(dialOpts, grpc.WithTransportCredentials(insecure.NewCredentials()))
|
|
} else {
|
|
dialOpts = append(dialOpts, grpc.WithTransportCredentials(credentials.NewTLS(&tls.Config{})))
|
|
}
|
|
|
|
conn, err := grpc.DialContext(dialCtx, cfg.Address, dialOpts...)
|
|
if err != nil {
|
|
return nil, merrors.InternalWrap(err, fmt.Sprintf("ledger: dial %s", cfg.Address))
|
|
}
|
|
|
|
return &ledgerClient{
|
|
cfg: cfg,
|
|
conn: conn,
|
|
client: connectorv1.NewConnectorServiceClient(conn),
|
|
}, nil
|
|
}
|
|
|
|
// NewWithClient injects a pre-built ledger client (useful for tests).
|
|
func NewWithClient(cfg Config, lc grpcConnectorClient) Client {
|
|
cfg.setDefaults()
|
|
return &ledgerClient{
|
|
cfg: cfg,
|
|
client: lc,
|
|
}
|
|
}
|
|
|
|
func (c *ledgerClient) Close() error {
|
|
if c.conn != nil {
|
|
return c.conn.Close()
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (c *ledgerClient) ReadBalance(ctx context.Context, accountID string) (*moneyv1.Money, error) {
|
|
if strings.TrimSpace(accountID) == "" {
|
|
return nil, merrors.InvalidArgument("ledger: account_id is required")
|
|
}
|
|
resp, err := c.GetBalance(ctx, &ledgerv1.GetBalanceRequest{LedgerAccountRef: strings.TrimSpace(accountID)})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if resp == nil || resp.GetBalance() == nil {
|
|
return nil, merrors.Internal("ledger: balance response missing")
|
|
}
|
|
return cloneMoney(resp.GetBalance()), nil
|
|
}
|
|
|
|
func (c *ledgerClient) CreateTransaction(ctx context.Context, tx rail.LedgerTx) (string, error) {
|
|
orgRef := strings.TrimSpace(tx.OrganizationRef)
|
|
if orgRef == "" {
|
|
return "", merrors.InvalidArgument("ledger: organization_ref is required")
|
|
}
|
|
accountRef := strings.TrimSpace(tx.LedgerAccountRef)
|
|
if accountRef == "" {
|
|
return "", merrors.InvalidArgument("ledger: ledger_account_ref is required")
|
|
}
|
|
money := &moneyv1.Money{
|
|
Currency: strings.TrimSpace(tx.Currency),
|
|
Amount: strings.TrimSpace(tx.Amount),
|
|
}
|
|
if money.GetCurrency() == "" || money.GetAmount() == "" {
|
|
return "", merrors.InvalidArgument("ledger: amount is required")
|
|
}
|
|
|
|
description := strings.TrimSpace(tx.Description)
|
|
metadata := ledgerTxMetadata(tx.Metadata, tx)
|
|
|
|
switch {
|
|
case isLedgerRail(tx.FromRail) && !isLedgerRail(tx.ToRail):
|
|
resp, err := c.PostDebitWithCharges(ctx, &ledgerv1.PostDebitRequest{
|
|
IdempotencyKey: strings.TrimSpace(tx.IdempotencyKey),
|
|
OrganizationRef: orgRef,
|
|
LedgerAccountRef: accountRef,
|
|
Money: money,
|
|
Description: description,
|
|
Charges: tx.Charges,
|
|
Metadata: metadata,
|
|
ContraLedgerAccountRef: strings.TrimSpace(tx.ContraLedgerAccountRef),
|
|
})
|
|
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{
|
|
IdempotencyKey: strings.TrimSpace(tx.IdempotencyKey),
|
|
OrganizationRef: orgRef,
|
|
LedgerAccountRef: accountRef,
|
|
Money: money,
|
|
Description: description,
|
|
Charges: tx.Charges,
|
|
Metadata: metadata,
|
|
ContraLedgerAccountRef: strings.TrimSpace(tx.ContraLedgerAccountRef),
|
|
})
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return strings.TrimSpace(resp.GetJournalEntryRef()), nil
|
|
default:
|
|
return "", merrors.InvalidArgument("ledger: unsupported transaction direction")
|
|
}
|
|
}
|
|
|
|
func (c *ledgerClient) HoldBalance(ctx context.Context, accountID string, amount string) error {
|
|
return merrors.NotImplemented("ledger: hold balance not supported")
|
|
}
|
|
|
|
func (c *ledgerClient) CreateAccount(ctx context.Context, req *ledgerv1.CreateAccountRequest) (*ledgerv1.CreateAccountResponse, error) {
|
|
ctx, cancel := c.callContext(ctx)
|
|
defer cancel()
|
|
if req == nil {
|
|
return nil, merrors.InvalidArgument("ledger: request is required")
|
|
}
|
|
if strings.TrimSpace(req.GetCurrency()) == "" {
|
|
return nil, merrors.InvalidArgument("ledger: currency is required")
|
|
}
|
|
params := map[string]interface{}{
|
|
"organization_ref": strings.TrimSpace(req.GetOrganizationRef()),
|
|
"account_code": strings.TrimSpace(req.GetAccountCode()),
|
|
"account_type": req.GetAccountType().String(),
|
|
"status": req.GetStatus().String(),
|
|
"allow_negative": req.GetAllowNegative(),
|
|
"is_settlement": req.GetIsSettlement(),
|
|
}
|
|
if len(req.GetMetadata()) > 0 {
|
|
params["metadata"] = mapStringToInterface(req.GetMetadata())
|
|
}
|
|
resp, err := c.client.OpenAccount(ctx, &connectorv1.OpenAccountRequest{
|
|
Kind: connectorv1.AccountKind_LEDGER_ACCOUNT,
|
|
Asset: strings.TrimSpace(req.GetCurrency()),
|
|
Params: structFromMap(params),
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if resp.GetError() != nil {
|
|
return nil, connectorError(resp.GetError())
|
|
}
|
|
return &ledgerv1.CreateAccountResponse{Account: ledgerAccountFromConnector(resp.GetAccount())}, nil
|
|
}
|
|
|
|
func (c *ledgerClient) ListAccounts(ctx context.Context, req *ledgerv1.ListAccountsRequest) (*ledgerv1.ListAccountsResponse, error) {
|
|
ctx, cancel := c.callContext(ctx)
|
|
defer cancel()
|
|
if req == nil || strings.TrimSpace(req.GetOrganizationRef()) == "" {
|
|
return nil, merrors.InvalidArgument("ledger: organization_ref is required")
|
|
}
|
|
resp, err := c.client.ListAccounts(ctx, &connectorv1.ListAccountsRequest{OwnerRef: strings.TrimSpace(req.GetOrganizationRef())})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
accounts := make([]*ledgerv1.LedgerAccount, 0, len(resp.GetAccounts()))
|
|
for _, account := range resp.GetAccounts() {
|
|
accounts = append(accounts, ledgerAccountFromConnector(account))
|
|
}
|
|
return &ledgerv1.ListAccountsResponse{Accounts: accounts}, nil
|
|
}
|
|
|
|
func (c *ledgerClient) PostCreditWithCharges(ctx context.Context, req *ledgerv1.PostCreditRequest) (*ledgerv1.PostResponse, error) {
|
|
return c.submitLedgerOperation(ctx, connectorv1.OperationType_CREDIT, "", req.GetLedgerAccountRef(), req.GetMoney(), req)
|
|
}
|
|
|
|
func (c *ledgerClient) PostDebitWithCharges(ctx context.Context, req *ledgerv1.PostDebitRequest) (*ledgerv1.PostResponse, error) {
|
|
return c.submitLedgerOperation(ctx, connectorv1.OperationType_DEBIT, req.GetLedgerAccountRef(), "", req.GetMoney(), req)
|
|
}
|
|
|
|
func (c *ledgerClient) TransferInternal(ctx context.Context, req *ledgerv1.TransferRequest) (*ledgerv1.PostResponse, error) {
|
|
return c.submitLedgerOperation(ctx, connectorv1.OperationType_TRANSFER, req.GetFromLedgerAccountRef(), req.GetToLedgerAccountRef(), req.GetMoney(), req)
|
|
}
|
|
|
|
func (c *ledgerClient) ApplyFXWithCharges(ctx context.Context, req *ledgerv1.FXRequest) (*ledgerv1.PostResponse, error) {
|
|
ctx, cancel := c.callContext(ctx)
|
|
defer cancel()
|
|
if req == nil {
|
|
return nil, merrors.InvalidArgument("ledger: request is required")
|
|
}
|
|
if req.GetFromMoney() == nil || req.GetToMoney() == nil {
|
|
return nil, merrors.InvalidArgument("ledger: from_money and to_money are required")
|
|
}
|
|
params := ledgerOperationParams(req.GetOrganizationRef(), req.GetDescription(), req.GetMetadata(), req.GetCharges(), req.GetEventTime())
|
|
params["rate"] = strings.TrimSpace(req.GetRate())
|
|
params["to_money"] = map[string]interface{}{"amount": req.GetToMoney().GetAmount(), "currency": req.GetToMoney().GetCurrency()}
|
|
operation := &connectorv1.Operation{
|
|
Type: connectorv1.OperationType_FX,
|
|
IdempotencyKey: strings.TrimSpace(req.GetIdempotencyKey()),
|
|
From: accountParty(req.GetFromLedgerAccountRef()),
|
|
To: accountParty(req.GetToLedgerAccountRef()),
|
|
Money: req.GetFromMoney(),
|
|
Params: structFromMap(params),
|
|
}
|
|
resp, err := c.client.SubmitOperation(ctx, &connectorv1.SubmitOperationRequest{Operation: operation})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if resp.GetReceipt() != nil && resp.GetReceipt().GetError() != nil {
|
|
return nil, connectorError(resp.GetReceipt().GetError())
|
|
}
|
|
return &ledgerv1.PostResponse{JournalEntryRef: resp.GetReceipt().GetOperationId(), EntryType: ledgerv1.EntryType_ENTRY_FX}, nil
|
|
}
|
|
|
|
func (c *ledgerClient) GetBalance(ctx context.Context, req *ledgerv1.GetBalanceRequest) (*ledgerv1.BalanceResponse, 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")
|
|
}
|
|
resp, err := c.client.GetBalance(ctx, &connectorv1.GetBalanceRequest{AccountRef: &connectorv1.AccountRef{ConnectorId: ledgerConnectorID, AccountId: strings.TrimSpace(req.GetLedgerAccountRef())}})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
balance := resp.GetBalance()
|
|
if balance == nil {
|
|
return nil, merrors.Internal("ledger: balance response missing")
|
|
}
|
|
return &ledgerv1.BalanceResponse{
|
|
LedgerAccountRef: strings.TrimSpace(req.GetLedgerAccountRef()),
|
|
Balance: balance.GetAvailable(),
|
|
LastUpdated: balance.GetCalculatedAt(),
|
|
}, nil
|
|
}
|
|
|
|
func (c *ledgerClient) GetJournalEntry(ctx context.Context, req *ledgerv1.GetEntryRequest) (*ledgerv1.JournalEntryResponse, error) {
|
|
ctx, cancel := c.callContext(ctx)
|
|
defer cancel()
|
|
if req == nil || strings.TrimSpace(req.GetEntryRef()) == "" {
|
|
return nil, merrors.InvalidArgument("ledger: entry_ref is required")
|
|
}
|
|
resp, err := c.client.GetOperation(ctx, &connectorv1.GetOperationRequest{OperationId: strings.TrimSpace(req.GetEntryRef())})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return journalEntryFromOperation(resp.GetOperation()), nil
|
|
}
|
|
|
|
func (c *ledgerClient) GetStatement(ctx context.Context, req *ledgerv1.GetStatementRequest) (*ledgerv1.StatementResponse, 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")
|
|
}
|
|
resp, err := c.client.ListOperations(ctx, &connectorv1.ListOperationsRequest{
|
|
AccountRef: &connectorv1.AccountRef{ConnectorId: ledgerConnectorID, AccountId: strings.TrimSpace(req.GetLedgerAccountRef())},
|
|
Page: pageFromStatement(req),
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
entries := make([]*ledgerv1.JournalEntryResponse, 0, len(resp.GetOperations()))
|
|
for _, op := range resp.GetOperations() {
|
|
entries = append(entries, journalEntryFromOperation(op))
|
|
}
|
|
nextCursor := ""
|
|
if resp.GetPage() != nil {
|
|
nextCursor = resp.GetPage().GetNextCursor()
|
|
}
|
|
return &ledgerv1.StatementResponse{Entries: entries, NextCursor: nextCursor}, nil
|
|
}
|
|
|
|
func (c *ledgerClient) submitLedgerOperation(ctx context.Context, opType connectorv1.OperationType, fromRef, toRef string, money *moneyv1.Money, req interface{}) (*ledgerv1.PostResponse, error) {
|
|
ctx, cancel := c.callContext(ctx)
|
|
defer cancel()
|
|
if money == nil {
|
|
return nil, merrors.InvalidArgument("ledger: money is required")
|
|
}
|
|
|
|
var (
|
|
idempotencyKey string
|
|
orgRef string
|
|
description string
|
|
metadata map[string]string
|
|
charges []*ledgerv1.PostingLine
|
|
eventTime *timestamppb.Timestamp
|
|
contraRef string
|
|
)
|
|
|
|
switch r := req.(type) {
|
|
case *ledgerv1.PostCreditRequest:
|
|
idempotencyKey = r.GetIdempotencyKey()
|
|
orgRef = r.GetOrganizationRef()
|
|
description = r.GetDescription()
|
|
metadata = r.GetMetadata()
|
|
charges = r.GetCharges()
|
|
eventTime = r.GetEventTime()
|
|
contraRef = r.GetContraLedgerAccountRef()
|
|
case *ledgerv1.PostDebitRequest:
|
|
idempotencyKey = r.GetIdempotencyKey()
|
|
orgRef = r.GetOrganizationRef()
|
|
description = r.GetDescription()
|
|
metadata = r.GetMetadata()
|
|
charges = r.GetCharges()
|
|
eventTime = r.GetEventTime()
|
|
contraRef = r.GetContraLedgerAccountRef()
|
|
case *ledgerv1.TransferRequest:
|
|
idempotencyKey = r.GetIdempotencyKey()
|
|
orgRef = r.GetOrganizationRef()
|
|
description = r.GetDescription()
|
|
metadata = r.GetMetadata()
|
|
charges = r.GetCharges()
|
|
eventTime = r.GetEventTime()
|
|
}
|
|
|
|
params := ledgerOperationParams(orgRef, description, metadata, charges, eventTime)
|
|
if contraRef != "" {
|
|
params["contra_ledger_account_ref"] = strings.TrimSpace(contraRef)
|
|
}
|
|
|
|
op := &connectorv1.Operation{
|
|
Type: opType,
|
|
IdempotencyKey: strings.TrimSpace(idempotencyKey),
|
|
Money: money,
|
|
Params: structFromMap(params),
|
|
}
|
|
if fromRef != "" {
|
|
op.From = accountParty(fromRef)
|
|
}
|
|
if toRef != "" {
|
|
op.To = accountParty(toRef)
|
|
}
|
|
|
|
resp, err := c.client.SubmitOperation(ctx, &connectorv1.SubmitOperationRequest{Operation: op})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if resp.GetReceipt() != nil && resp.GetReceipt().GetError() != nil {
|
|
return nil, connectorError(resp.GetReceipt().GetError())
|
|
}
|
|
return &ledgerv1.PostResponse{JournalEntryRef: resp.GetReceipt().GetOperationId(), EntryType: entryTypeFromOperation(opType)}, nil
|
|
}
|
|
|
|
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),
|
|
"description": strings.TrimSpace(description),
|
|
}
|
|
if len(metadata) > 0 {
|
|
params["metadata"] = mapStringToInterface(metadata)
|
|
}
|
|
if len(charges) > 0 {
|
|
params["charges"] = chargesToInterface(charges)
|
|
}
|
|
if eventTime != nil {
|
|
params["event_time"] = eventTime.AsTime().UTC().Format(time.RFC3339Nano)
|
|
}
|
|
return params
|
|
}
|
|
|
|
func accountParty(accountRef string) *connectorv1.OperationParty {
|
|
if strings.TrimSpace(accountRef) == "" {
|
|
return nil
|
|
}
|
|
return &connectorv1.OperationParty{Ref: &connectorv1.OperationParty_Account{Account: &connectorv1.AccountRef{ConnectorId: ledgerConnectorID, AccountId: strings.TrimSpace(accountRef)}}}
|
|
}
|
|
|
|
func entryTypeFromOperation(opType connectorv1.OperationType) ledgerv1.EntryType {
|
|
switch opType {
|
|
case connectorv1.OperationType_CREDIT:
|
|
return ledgerv1.EntryType_ENTRY_CREDIT
|
|
case connectorv1.OperationType_DEBIT:
|
|
return ledgerv1.EntryType_ENTRY_DEBIT
|
|
case connectorv1.OperationType_TRANSFER:
|
|
return ledgerv1.EntryType_ENTRY_TRANSFER
|
|
case connectorv1.OperationType_FX:
|
|
return ledgerv1.EntryType_ENTRY_FX
|
|
default:
|
|
return ledgerv1.EntryType_ENTRY_TYPE_UNSPECIFIED
|
|
}
|
|
}
|
|
|
|
func ledgerAccountFromConnector(account *connectorv1.Account) *ledgerv1.LedgerAccount {
|
|
if account == nil {
|
|
return nil
|
|
}
|
|
details := map[string]interface{}{}
|
|
if account.GetProviderDetails() != nil {
|
|
details = account.GetProviderDetails().AsMap()
|
|
}
|
|
accountType := ledgerv1.AccountType_ACCOUNT_TYPE_UNSPECIFIED
|
|
if v := strings.TrimSpace(fmt.Sprint(details["account_type"])); v != "" {
|
|
accountType = parseAccountType(v)
|
|
}
|
|
status := ledgerv1.AccountStatus_ACCOUNT_STATUS_UNSPECIFIED
|
|
if v := strings.TrimSpace(fmt.Sprint(details["status"])); v != "" {
|
|
status = parseAccountStatus(v)
|
|
}
|
|
allowNegative := false
|
|
if v, ok := details["allow_negative"].(bool); ok {
|
|
allowNegative = v
|
|
}
|
|
isSettlement := false
|
|
if v, ok := details["is_settlement"].(bool); ok {
|
|
isSettlement = v
|
|
}
|
|
accountCode := strings.TrimSpace(fmt.Sprint(details["account_code"]))
|
|
accountID := ""
|
|
if ref := account.GetRef(); ref != nil {
|
|
accountID = strings.TrimSpace(ref.GetAccountId())
|
|
}
|
|
return &ledgerv1.LedgerAccount{
|
|
LedgerAccountRef: accountID,
|
|
OrganizationRef: strings.TrimSpace(account.GetOwnerRef()),
|
|
AccountCode: accountCode,
|
|
AccountType: accountType,
|
|
Currency: strings.TrimSpace(account.GetAsset()),
|
|
Status: status,
|
|
AllowNegative: allowNegative,
|
|
IsSettlement: isSettlement,
|
|
CreatedAt: account.GetCreatedAt(),
|
|
UpdatedAt: account.GetUpdatedAt(),
|
|
}
|
|
}
|
|
|
|
func parseAccountType(value string) ledgerv1.AccountType {
|
|
switch strings.ToUpper(strings.TrimSpace(value)) {
|
|
case "ACCOUNT_TYPE_ASSET", "ASSET":
|
|
return ledgerv1.AccountType_ACCOUNT_TYPE_ASSET
|
|
case "ACCOUNT_TYPE_LIABILITY", "LIABILITY":
|
|
return ledgerv1.AccountType_ACCOUNT_TYPE_LIABILITY
|
|
case "ACCOUNT_TYPE_REVENUE", "REVENUE":
|
|
return ledgerv1.AccountType_ACCOUNT_TYPE_REVENUE
|
|
case "ACCOUNT_TYPE_EXPENSE", "EXPENSE":
|
|
return ledgerv1.AccountType_ACCOUNT_TYPE_EXPENSE
|
|
default:
|
|
return ledgerv1.AccountType_ACCOUNT_TYPE_UNSPECIFIED
|
|
}
|
|
}
|
|
|
|
func parseAccountStatus(value string) ledgerv1.AccountStatus {
|
|
switch strings.ToUpper(strings.TrimSpace(value)) {
|
|
case "ACCOUNT_STATUS_ACTIVE", "ACTIVE":
|
|
return ledgerv1.AccountStatus_ACCOUNT_STATUS_ACTIVE
|
|
case "ACCOUNT_STATUS_FROZEN", "FROZEN":
|
|
return ledgerv1.AccountStatus_ACCOUNT_STATUS_FROZEN
|
|
default:
|
|
return ledgerv1.AccountStatus_ACCOUNT_STATUS_UNSPECIFIED
|
|
}
|
|
}
|
|
|
|
func journalEntryFromOperation(op *connectorv1.Operation) *ledgerv1.JournalEntryResponse {
|
|
if op == nil {
|
|
return nil
|
|
}
|
|
entry := &ledgerv1.JournalEntryResponse{
|
|
EntryRef: strings.TrimSpace(op.GetOperationId()),
|
|
EntryType: entryTypeFromOperation(op.GetType()),
|
|
Description: operationDescription(op),
|
|
EventTime: op.GetCreatedAt(),
|
|
Lines: postingLinesFromOperation(op),
|
|
LedgerAccountRefs: ledgerAccountRefsFromOperation(op),
|
|
}
|
|
return entry
|
|
}
|
|
|
|
func operationDescription(op *connectorv1.Operation) string {
|
|
if op == nil || op.GetParams() == nil {
|
|
return ""
|
|
}
|
|
if value, ok := op.GetParams().AsMap()["description"]; ok {
|
|
return strings.TrimSpace(fmt.Sprint(value))
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func postingLinesFromOperation(op *connectorv1.Operation) []*ledgerv1.PostingLine {
|
|
if op == nil || op.GetMoney() == nil {
|
|
return nil
|
|
}
|
|
lines := []*ledgerv1.PostingLine{}
|
|
if from := op.GetFrom(); from != nil && from.GetAccount() != nil {
|
|
lines = append(lines, &ledgerv1.PostingLine{LedgerAccountRef: strings.TrimSpace(from.GetAccount().GetAccountId()), Money: op.GetMoney(), LineType: ledgerv1.LineType_LINE_MAIN})
|
|
}
|
|
if to := op.GetTo(); to != nil && to.GetAccount() != nil {
|
|
lines = append(lines, &ledgerv1.PostingLine{LedgerAccountRef: strings.TrimSpace(to.GetAccount().GetAccountId()), Money: op.GetMoney(), LineType: ledgerv1.LineType_LINE_MAIN})
|
|
}
|
|
if len(lines) == 0 {
|
|
lines = append(lines, &ledgerv1.PostingLine{Money: op.GetMoney(), LineType: ledgerv1.LineType_LINE_MAIN})
|
|
}
|
|
return lines
|
|
}
|
|
|
|
func ledgerAccountRefsFromOperation(op *connectorv1.Operation) []string {
|
|
refs := []string{}
|
|
if op == nil {
|
|
return refs
|
|
}
|
|
if from := op.GetFrom(); from != nil && from.GetAccount() != nil {
|
|
refs = append(refs, strings.TrimSpace(from.GetAccount().GetAccountId()))
|
|
}
|
|
if to := op.GetTo(); to != nil && to.GetAccount() != nil {
|
|
refs = append(refs, strings.TrimSpace(to.GetAccount().GetAccountId()))
|
|
}
|
|
return refs
|
|
}
|
|
|
|
func pageFromStatement(req *ledgerv1.GetStatementRequest) *paginationv1.CursorPageRequest {
|
|
if req == nil {
|
|
return nil
|
|
}
|
|
return &paginationv1.CursorPageRequest{
|
|
Cursor: strings.TrimSpace(req.GetCursor()),
|
|
Limit: req.GetLimit(),
|
|
}
|
|
}
|
|
|
|
func chargesToInterface(charges []*ledgerv1.PostingLine) []interface{} {
|
|
if len(charges) == 0 {
|
|
return nil
|
|
}
|
|
result := make([]interface{}, 0, len(charges))
|
|
for _, line := range charges {
|
|
if line == nil || line.GetMoney() == nil {
|
|
continue
|
|
}
|
|
result = append(result, map[string]interface{}{
|
|
"ledger_account_ref": strings.TrimSpace(line.GetLedgerAccountRef()),
|
|
"amount": strings.TrimSpace(line.GetMoney().GetAmount()),
|
|
"currency": strings.TrimSpace(line.GetMoney().GetCurrency()),
|
|
"line_type": line.GetLineType().String(),
|
|
})
|
|
}
|
|
if len(result) == 0 {
|
|
return nil
|
|
}
|
|
return result
|
|
}
|
|
|
|
func connectorError(err *connectorv1.ConnectorError) error {
|
|
if err == nil {
|
|
return nil
|
|
}
|
|
msg := strings.TrimSpace(err.GetMessage())
|
|
switch err.GetCode() {
|
|
case connectorv1.ErrorCode_INVALID_PARAMS:
|
|
return merrors.InvalidArgument(msg)
|
|
case connectorv1.ErrorCode_NOT_FOUND:
|
|
return merrors.NoData(msg)
|
|
case connectorv1.ErrorCode_UNSUPPORTED_OPERATION, connectorv1.ErrorCode_UNSUPPORTED_ACCOUNT_KIND:
|
|
return merrors.NotImplemented(msg)
|
|
case connectorv1.ErrorCode_RATE_LIMITED, connectorv1.ErrorCode_TEMPORARY_UNAVAILABLE:
|
|
return merrors.Internal(msg)
|
|
default:
|
|
return merrors.Internal(msg)
|
|
}
|
|
}
|
|
|
|
func structFromMap(data map[string]interface{}) *structpb.Struct {
|
|
if len(data) == 0 {
|
|
return nil
|
|
}
|
|
result, err := structpb.NewStruct(data)
|
|
if err != nil {
|
|
return nil
|
|
}
|
|
return result
|
|
}
|
|
|
|
func mapStringToInterface(input map[string]string) map[string]interface{} {
|
|
if len(input) == 0 {
|
|
return nil
|
|
}
|
|
out := make(map[string]interface{}, len(input))
|
|
for k, v := range input {
|
|
out[k] = v
|
|
}
|
|
return out
|
|
}
|
|
|
|
func (c *ledgerClient) callContext(ctx context.Context) (context.Context, context.CancelFunc) {
|
|
timeout := c.cfg.CallTimeout
|
|
if timeout <= 0 {
|
|
timeout = 3 * time.Second
|
|
}
|
|
return context.WithTimeout(ctx, timeout)
|
|
}
|
|
|
|
func isLedgerRail(value string) bool {
|
|
return strings.EqualFold(strings.TrimSpace(value), "LEDGER")
|
|
}
|
|
|
|
func cloneMoney(input *moneyv1.Money) *moneyv1.Money {
|
|
if input == nil {
|
|
return nil
|
|
}
|
|
return &moneyv1.Money{
|
|
Currency: input.GetCurrency(),
|
|
Amount: input.GetAmount(),
|
|
}
|
|
}
|
|
|
|
func cloneMetadata(input map[string]string) map[string]string {
|
|
if len(input) == 0 {
|
|
return nil
|
|
}
|
|
out := make(map[string]string, len(input))
|
|
for k, v := range input {
|
|
out[k] = v
|
|
}
|
|
return out
|
|
}
|
|
|
|
func ledgerTxMetadata(base map[string]string, tx rail.LedgerTx) map[string]string {
|
|
meta := cloneMetadata(base)
|
|
if meta == nil {
|
|
meta = map[string]string{}
|
|
}
|
|
if val := strings.TrimSpace(tx.PaymentPlanID); val != "" {
|
|
meta["payment_plan_id"] = val
|
|
}
|
|
if val := strings.TrimSpace(tx.FromRail); val != "" {
|
|
meta["from_rail"] = val
|
|
}
|
|
if val := strings.TrimSpace(tx.ToRail); val != "" {
|
|
meta["to_rail"] = val
|
|
}
|
|
if val := strings.TrimSpace(tx.ExternalReferenceID); val != "" {
|
|
meta["external_reference_id"] = val
|
|
}
|
|
if val := strings.TrimSpace(tx.FXRateUsed); val != "" {
|
|
meta["fx_rate_used"] = val
|
|
}
|
|
if val := strings.TrimSpace(tx.FeeAmount); val != "" {
|
|
meta["fee_amount"] = val
|
|
}
|
|
if len(meta) == 0 {
|
|
return nil
|
|
}
|
|
return meta
|
|
}
|