interface refactoring
This commit is contained in:
@@ -9,14 +9,19 @@ import (
|
||||
|
||||
"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"
|
||||
unifiedv1 "github.com/tech/sendico/pkg/proto/gateway/unified/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)
|
||||
@@ -37,22 +42,20 @@ type Client interface {
|
||||
Close() error
|
||||
}
|
||||
|
||||
type grpcLedgerClient interface {
|
||||
CreateAccount(ctx context.Context, in *ledgerv1.CreateAccountRequest, opts ...grpc.CallOption) (*ledgerv1.CreateAccountResponse, error)
|
||||
ListAccounts(ctx context.Context, in *ledgerv1.ListAccountsRequest, opts ...grpc.CallOption) (*ledgerv1.ListAccountsResponse, error)
|
||||
PostCreditWithCharges(ctx context.Context, in *ledgerv1.PostCreditRequest, opts ...grpc.CallOption) (*ledgerv1.PostResponse, error)
|
||||
PostDebitWithCharges(ctx context.Context, in *ledgerv1.PostDebitRequest, opts ...grpc.CallOption) (*ledgerv1.PostResponse, error)
|
||||
TransferInternal(ctx context.Context, in *ledgerv1.TransferRequest, opts ...grpc.CallOption) (*ledgerv1.PostResponse, error)
|
||||
ApplyFXWithCharges(ctx context.Context, in *ledgerv1.FXRequest, opts ...grpc.CallOption) (*ledgerv1.PostResponse, error)
|
||||
GetBalance(ctx context.Context, in *ledgerv1.GetBalanceRequest, opts ...grpc.CallOption) (*ledgerv1.BalanceResponse, error)
|
||||
GetJournalEntry(ctx context.Context, in *ledgerv1.GetEntryRequest, opts ...grpc.CallOption) (*ledgerv1.JournalEntryResponse, error)
|
||||
GetStatement(ctx context.Context, in *ledgerv1.GetStatementRequest, opts ...grpc.CallOption) (*ledgerv1.StatementResponse, 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 grpcLedgerClient
|
||||
client grpcConnectorClient
|
||||
}
|
||||
|
||||
// New dials the ledger endpoint and returns a ready client.
|
||||
@@ -82,12 +85,12 @@ func New(ctx context.Context, cfg Config, opts ...grpc.DialOption) (Client, erro
|
||||
return &ledgerClient{
|
||||
cfg: cfg,
|
||||
conn: conn,
|
||||
client: unifiedv1.NewUnifiedGatewayServiceClient(conn),
|
||||
client: connectorv1.NewConnectorServiceClient(conn),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// NewWithClient injects a pre-built ledger client (useful for tests).
|
||||
func NewWithClient(cfg Config, lc grpcLedgerClient) Client {
|
||||
func NewWithClient(cfg Config, lc grpcConnectorClient) Client {
|
||||
cfg.setDefaults()
|
||||
return &ledgerClient{
|
||||
cfg: cfg,
|
||||
@@ -179,55 +182,460 @@ func (c *ledgerClient) HoldBalance(ctx context.Context, accountID string, amount
|
||||
func (c *ledgerClient) CreateAccount(ctx context.Context, req *ledgerv1.CreateAccountRequest) (*ledgerv1.CreateAccountResponse, error) {
|
||||
ctx, cancel := c.callContext(ctx)
|
||||
defer cancel()
|
||||
return c.client.CreateAccount(ctx, req)
|
||||
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()
|
||||
return c.client.ListAccounts(ctx, req)
|
||||
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) {
|
||||
ctx, cancel := c.callContext(ctx)
|
||||
defer cancel()
|
||||
return c.client.PostCreditWithCharges(ctx, req)
|
||||
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) {
|
||||
ctx, cancel := c.callContext(ctx)
|
||||
defer cancel()
|
||||
return c.client.PostDebitWithCharges(ctx, req)
|
||||
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) {
|
||||
ctx, cancel := c.callContext(ctx)
|
||||
defer cancel()
|
||||
return c.client.TransferInternal(ctx, req)
|
||||
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()
|
||||
return c.client.ApplyFXWithCharges(ctx, req)
|
||||
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()
|
||||
return c.client.GetBalance(ctx, req)
|
||||
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()
|
||||
return c.client.GetJournalEntry(ctx, req)
|
||||
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()
|
||||
return c.client.GetStatement(ctx, req)
|
||||
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) {
|
||||
|
||||
622
api/ledger/internal/service/ledger/connector.go
Normal file
622
api/ledger/internal/service/ledger/connector.go
Normal file
@@ -0,0 +1,622 @@
|
||||
package ledger
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/tech/sendico/ledger/internal/appversion"
|
||||
"github.com/tech/sendico/pkg/connector/params"
|
||||
"github.com/tech/sendico/pkg/merrors"
|
||||
connectorv1 "github.com/tech/sendico/pkg/proto/connector/v1"
|
||||
moneyv1 "github.com/tech/sendico/pkg/proto/common/money/v1"
|
||||
ledgerv1 "github.com/tech/sendico/pkg/proto/ledger/v1"
|
||||
"google.golang.org/protobuf/types/known/structpb"
|
||||
"google.golang.org/protobuf/types/known/timestamppb"
|
||||
)
|
||||
|
||||
const ledgerConnectorID = "ledger"
|
||||
|
||||
type connectorAdapter struct {
|
||||
svc *Service
|
||||
}
|
||||
|
||||
func newConnectorAdapter(svc *Service) *connectorAdapter {
|
||||
return &connectorAdapter{svc: svc}
|
||||
}
|
||||
|
||||
func (c *connectorAdapter) GetCapabilities(_ context.Context, _ *connectorv1.GetCapabilitiesRequest) (*connectorv1.GetCapabilitiesResponse, error) {
|
||||
return &connectorv1.GetCapabilitiesResponse{
|
||||
Capabilities: &connectorv1.ConnectorCapabilities{
|
||||
ConnectorType: ledgerConnectorID,
|
||||
Version: appversion.Create().Short(),
|
||||
SupportedAccountKinds: []connectorv1.AccountKind{connectorv1.AccountKind_LEDGER_ACCOUNT},
|
||||
SupportedOperationTypes: []connectorv1.OperationType{
|
||||
connectorv1.OperationType_CREDIT,
|
||||
connectorv1.OperationType_DEBIT,
|
||||
connectorv1.OperationType_TRANSFER,
|
||||
connectorv1.OperationType_FX,
|
||||
},
|
||||
OpenAccountParams: ledgerOpenAccountParams(),
|
||||
OperationParams: ledgerOperationParams(),
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (c *connectorAdapter) OpenAccount(ctx context.Context, req *connectorv1.OpenAccountRequest) (*connectorv1.OpenAccountResponse, error) {
|
||||
if req == nil {
|
||||
return &connectorv1.OpenAccountResponse{Error: connectorError(connectorv1.ErrorCode_INVALID_PARAMS, "open_account: request is required", nil, "")}, nil
|
||||
}
|
||||
if req.GetKind() != connectorv1.AccountKind_LEDGER_ACCOUNT {
|
||||
return &connectorv1.OpenAccountResponse{Error: connectorError(connectorv1.ErrorCode_UNSUPPORTED_ACCOUNT_KIND, "open_account: unsupported account kind", nil, "")}, nil
|
||||
}
|
||||
|
||||
reader := params.New(req.GetParams())
|
||||
orgRef := strings.TrimSpace(reader.String("organization_ref"))
|
||||
accountCode := strings.TrimSpace(reader.String("account_code"))
|
||||
if orgRef == "" || accountCode == "" {
|
||||
return &connectorv1.OpenAccountResponse{Error: connectorError(connectorv1.ErrorCode_INVALID_PARAMS, "open_account: organization_ref and account_code are required", nil, "")}, nil
|
||||
}
|
||||
|
||||
accountType, err := parseLedgerAccountType(reader, "account_type")
|
||||
if err != nil {
|
||||
return &connectorv1.OpenAccountResponse{Error: connectorError(connectorv1.ErrorCode_INVALID_PARAMS, err.Error(), nil, "")}, nil
|
||||
}
|
||||
|
||||
currency := strings.TrimSpace(req.GetAsset())
|
||||
if currency == "" {
|
||||
currency = strings.TrimSpace(reader.String("currency"))
|
||||
}
|
||||
if currency == "" {
|
||||
return &connectorv1.OpenAccountResponse{Error: connectorError(connectorv1.ErrorCode_INVALID_PARAMS, "open_account: asset is required", nil, "")}, nil
|
||||
}
|
||||
|
||||
status := parseLedgerAccountStatus(reader, "status")
|
||||
metadata := mergeMetadata(reader.StringMap("metadata"), req.GetLabel(), req.GetOwnerRef(), req.GetCorrelationId(), req.GetParentIntentId())
|
||||
|
||||
resp, err := c.svc.CreateAccount(ctx, &ledgerv1.CreateAccountRequest{
|
||||
OrganizationRef: orgRef,
|
||||
AccountCode: accountCode,
|
||||
AccountType: accountType,
|
||||
Currency: currency,
|
||||
Status: status,
|
||||
AllowNegative: reader.Bool("allow_negative"),
|
||||
IsSettlement: reader.Bool("is_settlement"),
|
||||
Metadata: metadata,
|
||||
})
|
||||
if err != nil {
|
||||
return &connectorv1.OpenAccountResponse{Error: connectorError(mapErrorCode(err), err.Error(), nil, "")}, nil
|
||||
}
|
||||
|
||||
return &connectorv1.OpenAccountResponse{
|
||||
Account: ledgerAccountToConnector(resp.GetAccount()),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (c *connectorAdapter) GetAccount(ctx context.Context, req *connectorv1.GetAccountRequest) (*connectorv1.GetAccountResponse, error) {
|
||||
if req == nil || req.GetAccountRef() == nil || strings.TrimSpace(req.GetAccountRef().GetAccountId()) == "" {
|
||||
return nil, merrors.InvalidArgument("get_account: account_ref.account_id is required")
|
||||
}
|
||||
accountRef, err := parseObjectID(strings.TrimSpace(req.GetAccountRef().GetAccountId()))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if c.svc.storage == nil || c.svc.storage.Accounts() == nil {
|
||||
return nil, merrors.Internal("get_account: storage unavailable")
|
||||
}
|
||||
account, err := c.svc.storage.Accounts().Get(ctx, accountRef)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &connectorv1.GetAccountResponse{
|
||||
Account: ledgerAccountToConnector(toProtoAccount(account)),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (c *connectorAdapter) ListAccounts(ctx context.Context, req *connectorv1.ListAccountsRequest) (*connectorv1.ListAccountsResponse, error) {
|
||||
if req == nil || strings.TrimSpace(req.GetOwnerRef()) == "" {
|
||||
return nil, merrors.InvalidArgument("list_accounts: owner_ref is required")
|
||||
}
|
||||
resp, err := c.svc.ListAccounts(ctx, &ledgerv1.ListAccountsRequest{OrganizationRef: strings.TrimSpace(req.GetOwnerRef())})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
accounts := make([]*connectorv1.Account, 0, len(resp.GetAccounts()))
|
||||
for _, account := range resp.GetAccounts() {
|
||||
accounts = append(accounts, ledgerAccountToConnector(account))
|
||||
}
|
||||
return &connectorv1.ListAccountsResponse{Accounts: accounts}, nil
|
||||
}
|
||||
|
||||
func (c *connectorAdapter) GetBalance(ctx context.Context, req *connectorv1.GetBalanceRequest) (*connectorv1.GetBalanceResponse, error) {
|
||||
if req == nil || req.GetAccountRef() == nil || strings.TrimSpace(req.GetAccountRef().GetAccountId()) == "" {
|
||||
return nil, merrors.InvalidArgument("get_balance: account_ref.account_id is required")
|
||||
}
|
||||
resp, err := c.svc.GetBalance(ctx, &ledgerv1.GetBalanceRequest{
|
||||
LedgerAccountRef: strings.TrimSpace(req.GetAccountRef().GetAccountId()),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &connectorv1.GetBalanceResponse{
|
||||
Balance: &connectorv1.Balance{
|
||||
AccountRef: req.GetAccountRef(),
|
||||
Available: resp.GetBalance(),
|
||||
CalculatedAt: resp.GetLastUpdated(),
|
||||
PendingInbound: nil,
|
||||
PendingOutbound: nil,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (c *connectorAdapter) SubmitOperation(ctx context.Context, req *connectorv1.SubmitOperationRequest) (*connectorv1.SubmitOperationResponse, error) {
|
||||
if req == nil || req.GetOperation() == nil {
|
||||
return &connectorv1.SubmitOperationResponse{Receipt: &connectorv1.OperationReceipt{Error: connectorError(connectorv1.ErrorCode_INVALID_PARAMS, "submit_operation: operation is required", nil, "")}}, nil
|
||||
}
|
||||
op := req.GetOperation()
|
||||
if strings.TrimSpace(op.GetIdempotencyKey()) == "" {
|
||||
return &connectorv1.SubmitOperationResponse{Receipt: &connectorv1.OperationReceipt{Error: connectorError(connectorv1.ErrorCode_INVALID_PARAMS, "submit_operation: idempotency_key is required", op, "")}}, nil
|
||||
}
|
||||
reader := params.New(op.GetParams())
|
||||
orgRef := strings.TrimSpace(reader.String("organization_ref"))
|
||||
if orgRef == "" {
|
||||
return &connectorv1.SubmitOperationResponse{Receipt: &connectorv1.OperationReceipt{Error: connectorError(connectorv1.ErrorCode_INVALID_PARAMS, "submit_operation: organization_ref is required", op, "")}}, nil
|
||||
}
|
||||
|
||||
metadata := mergeMetadata(reader.StringMap("metadata"), "", "", op.GetCorrelationId(), op.GetParentIntentId())
|
||||
description := strings.TrimSpace(reader.String("description"))
|
||||
eventTime := parseEventTime(reader)
|
||||
charges, err := parseLedgerCharges(reader)
|
||||
if err != nil {
|
||||
return &connectorv1.SubmitOperationResponse{Receipt: &connectorv1.OperationReceipt{Error: connectorError(connectorv1.ErrorCode_INVALID_PARAMS, err.Error(), op, "")}}, nil
|
||||
}
|
||||
|
||||
switch op.GetType() {
|
||||
case connectorv1.OperationType_CREDIT:
|
||||
accountID := operationAccountID(op.GetTo())
|
||||
if accountID == "" {
|
||||
return &connectorv1.SubmitOperationResponse{Receipt: &connectorv1.OperationReceipt{Error: connectorError(connectorv1.ErrorCode_INVALID_PARAMS, "credit: to.account is required", op, "")}}, nil
|
||||
}
|
||||
resp, err := c.svc.PostCreditWithCharges(ctx, &ledgerv1.PostCreditRequest{
|
||||
IdempotencyKey: strings.TrimSpace(op.GetIdempotencyKey()),
|
||||
OrganizationRef: orgRef,
|
||||
LedgerAccountRef: accountID,
|
||||
Money: op.GetMoney(),
|
||||
Description: description,
|
||||
Charges: charges,
|
||||
Metadata: metadata,
|
||||
EventTime: eventTime,
|
||||
ContraLedgerAccountRef: strings.TrimSpace(reader.String("contra_ledger_account_ref")),
|
||||
})
|
||||
if err != nil {
|
||||
return &connectorv1.SubmitOperationResponse{Receipt: &connectorv1.OperationReceipt{Error: connectorError(mapErrorCode(err), err.Error(), op, accountID)}}, nil
|
||||
}
|
||||
return &connectorv1.SubmitOperationResponse{Receipt: ledgerReceipt(resp.GetJournalEntryRef(), connectorv1.OperationStatus_CONFIRMED)}, nil
|
||||
case connectorv1.OperationType_DEBIT:
|
||||
accountID := operationAccountID(op.GetFrom())
|
||||
if accountID == "" {
|
||||
return &connectorv1.SubmitOperationResponse{Receipt: &connectorv1.OperationReceipt{Error: connectorError(connectorv1.ErrorCode_INVALID_PARAMS, "debit: from.account is required", op, "")}}, nil
|
||||
}
|
||||
resp, err := c.svc.PostDebitWithCharges(ctx, &ledgerv1.PostDebitRequest{
|
||||
IdempotencyKey: strings.TrimSpace(op.GetIdempotencyKey()),
|
||||
OrganizationRef: orgRef,
|
||||
LedgerAccountRef: accountID,
|
||||
Money: op.GetMoney(),
|
||||
Description: description,
|
||||
Charges: charges,
|
||||
Metadata: metadata,
|
||||
EventTime: eventTime,
|
||||
ContraLedgerAccountRef: strings.TrimSpace(reader.String("contra_ledger_account_ref")),
|
||||
})
|
||||
if err != nil {
|
||||
return &connectorv1.SubmitOperationResponse{Receipt: &connectorv1.OperationReceipt{Error: connectorError(mapErrorCode(err), err.Error(), op, accountID)}}, nil
|
||||
}
|
||||
return &connectorv1.SubmitOperationResponse{Receipt: ledgerReceipt(resp.GetJournalEntryRef(), connectorv1.OperationStatus_CONFIRMED)}, nil
|
||||
case connectorv1.OperationType_TRANSFER:
|
||||
fromID := operationAccountID(op.GetFrom())
|
||||
toID := operationAccountID(op.GetTo())
|
||||
if fromID == "" || toID == "" {
|
||||
return &connectorv1.SubmitOperationResponse{Receipt: &connectorv1.OperationReceipt{Error: connectorError(connectorv1.ErrorCode_INVALID_PARAMS, "transfer: from.account and to.account are required", op, "")}}, nil
|
||||
}
|
||||
resp, err := c.svc.TransferInternal(ctx, &ledgerv1.TransferRequest{
|
||||
IdempotencyKey: strings.TrimSpace(op.GetIdempotencyKey()),
|
||||
OrganizationRef: orgRef,
|
||||
FromLedgerAccountRef: fromID,
|
||||
ToLedgerAccountRef: toID,
|
||||
Money: op.GetMoney(),
|
||||
Description: description,
|
||||
Charges: charges,
|
||||
Metadata: metadata,
|
||||
EventTime: eventTime,
|
||||
})
|
||||
if err != nil {
|
||||
return &connectorv1.SubmitOperationResponse{Receipt: &connectorv1.OperationReceipt{Error: connectorError(mapErrorCode(err), err.Error(), op, "")}}, nil
|
||||
}
|
||||
return &connectorv1.SubmitOperationResponse{Receipt: ledgerReceipt(resp.GetJournalEntryRef(), connectorv1.OperationStatus_CONFIRMED)}, nil
|
||||
case connectorv1.OperationType_FX:
|
||||
fromID := operationAccountID(op.GetFrom())
|
||||
toID := operationAccountID(op.GetTo())
|
||||
if fromID == "" || toID == "" {
|
||||
return &connectorv1.SubmitOperationResponse{Receipt: &connectorv1.OperationReceipt{Error: connectorError(connectorv1.ErrorCode_INVALID_PARAMS, "fx: from.account and to.account are required", op, "")}}, nil
|
||||
}
|
||||
toMoney, err := parseMoneyFromMap(reader.Map("to_money"))
|
||||
if err != nil {
|
||||
return &connectorv1.SubmitOperationResponse{Receipt: &connectorv1.OperationReceipt{Error: connectorError(connectorv1.ErrorCode_INVALID_PARAMS, err.Error(), op, "")}}, nil
|
||||
}
|
||||
resp, err := c.svc.ApplyFXWithCharges(ctx, &ledgerv1.FXRequest{
|
||||
IdempotencyKey: strings.TrimSpace(op.GetIdempotencyKey()),
|
||||
OrganizationRef: orgRef,
|
||||
FromLedgerAccountRef: fromID,
|
||||
ToLedgerAccountRef: toID,
|
||||
FromMoney: op.GetMoney(),
|
||||
ToMoney: toMoney,
|
||||
Rate: strings.TrimSpace(reader.String("rate")),
|
||||
Description: description,
|
||||
Charges: charges,
|
||||
Metadata: metadata,
|
||||
EventTime: eventTime,
|
||||
})
|
||||
if err != nil {
|
||||
return &connectorv1.SubmitOperationResponse{Receipt: &connectorv1.OperationReceipt{Error: connectorError(mapErrorCode(err), err.Error(), op, "")}}, nil
|
||||
}
|
||||
return &connectorv1.SubmitOperationResponse{Receipt: ledgerReceipt(resp.GetJournalEntryRef(), connectorv1.OperationStatus_CONFIRMED)}, nil
|
||||
default:
|
||||
return &connectorv1.SubmitOperationResponse{Receipt: &connectorv1.OperationReceipt{Error: connectorError(connectorv1.ErrorCode_UNSUPPORTED_OPERATION, "submit_operation: unsupported operation type", op, "")}}, nil
|
||||
}
|
||||
}
|
||||
|
||||
func (c *connectorAdapter) GetOperation(ctx context.Context, req *connectorv1.GetOperationRequest) (*connectorv1.GetOperationResponse, error) {
|
||||
if req == nil || strings.TrimSpace(req.GetOperationId()) == "" {
|
||||
return nil, merrors.InvalidArgument("get_operation: operation_id is required")
|
||||
}
|
||||
entry, err := c.svc.GetJournalEntry(ctx, &ledgerv1.GetEntryRequest{EntryRef: strings.TrimSpace(req.GetOperationId())})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &connectorv1.GetOperationResponse{Operation: ledgerEntryToOperation(entry)}, nil
|
||||
}
|
||||
|
||||
func (c *connectorAdapter) ListOperations(ctx context.Context, req *connectorv1.ListOperationsRequest) (*connectorv1.ListOperationsResponse, error) {
|
||||
if req == nil || req.GetAccountRef() == nil || strings.TrimSpace(req.GetAccountRef().GetAccountId()) == "" {
|
||||
return nil, merrors.InvalidArgument("list_operations: account_ref.account_id is required")
|
||||
}
|
||||
resp, err := c.svc.GetStatement(ctx, &ledgerv1.GetStatementRequest{
|
||||
LedgerAccountRef: strings.TrimSpace(req.GetAccountRef().GetAccountId()),
|
||||
Cursor: "",
|
||||
Limit: 0,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ops := make([]*connectorv1.Operation, 0, len(resp.GetEntries()))
|
||||
for _, entry := range resp.GetEntries() {
|
||||
ops = append(ops, ledgerEntryToOperation(entry))
|
||||
}
|
||||
return &connectorv1.ListOperationsResponse{Operations: ops}, nil
|
||||
}
|
||||
|
||||
func ledgerOpenAccountParams() []*connectorv1.ParamSpec {
|
||||
return []*connectorv1.ParamSpec{
|
||||
{Key: "organization_ref", Type: connectorv1.ParamType_STRING, Required: true, Description: "Organization reference for the ledger account."},
|
||||
{Key: "account_code", Type: connectorv1.ParamType_STRING, Required: true, Description: "Ledger account code."},
|
||||
{Key: "account_type", Type: connectorv1.ParamType_STRING, Required: true, Description: "ASSET | LIABILITY | REVENUE | EXPENSE."},
|
||||
{Key: "status", Type: connectorv1.ParamType_STRING, Required: false, Description: "ACTIVE | FROZEN."},
|
||||
{Key: "allow_negative", Type: connectorv1.ParamType_BOOL, Required: false, Description: "Allow negative balance."},
|
||||
{Key: "is_settlement", Type: connectorv1.ParamType_BOOL, Required: false, Description: "Mark account as settlement."},
|
||||
{Key: "metadata", Type: connectorv1.ParamType_JSON, Required: false, Description: "Additional metadata map."},
|
||||
}
|
||||
}
|
||||
|
||||
func ledgerOperationParams() []*connectorv1.OperationParamSpec {
|
||||
common := []*connectorv1.ParamSpec{
|
||||
{Key: "organization_ref", Type: connectorv1.ParamType_STRING, Required: true, Description: "Organization reference."},
|
||||
{Key: "description", Type: connectorv1.ParamType_STRING, Required: false, Description: "Ledger entry description."},
|
||||
{Key: "metadata", Type: connectorv1.ParamType_JSON, Required: false, Description: "Entry metadata map."},
|
||||
{Key: "charges", Type: connectorv1.ParamType_JSON, Required: false, Description: "Posting line charges."},
|
||||
{Key: "event_time", Type: connectorv1.ParamType_STRING, Required: false, Description: "RFC3339 timestamp."},
|
||||
}
|
||||
return []*connectorv1.OperationParamSpec{
|
||||
{OperationType: connectorv1.OperationType_CREDIT, Params: append(common, &connectorv1.ParamSpec{Key: "contra_ledger_account_ref", Type: connectorv1.ParamType_STRING, Required: false})},
|
||||
{OperationType: connectorv1.OperationType_DEBIT, Params: append(common, &connectorv1.ParamSpec{Key: "contra_ledger_account_ref", Type: connectorv1.ParamType_STRING, Required: false})},
|
||||
{OperationType: connectorv1.OperationType_TRANSFER, Params: common},
|
||||
{OperationType: connectorv1.OperationType_FX, Params: append(common,
|
||||
&connectorv1.ParamSpec{Key: "to_money", Type: connectorv1.ParamType_JSON, Required: true, Description: "Target amount {amount,currency}."},
|
||||
&connectorv1.ParamSpec{Key: "rate", Type: connectorv1.ParamType_STRING, Required: false, Description: "FX rate snapshot."},
|
||||
)},
|
||||
}
|
||||
}
|
||||
|
||||
func ledgerAccountToConnector(account *ledgerv1.LedgerAccount) *connectorv1.Account {
|
||||
if account == nil {
|
||||
return nil
|
||||
}
|
||||
details, _ := structpb.NewStruct(map[string]interface{}{
|
||||
"account_code": account.GetAccountCode(),
|
||||
"account_type": account.GetAccountType().String(),
|
||||
"status": account.GetStatus().String(),
|
||||
"allow_negative": account.GetAllowNegative(),
|
||||
"is_settlement": account.GetIsSettlement(),
|
||||
})
|
||||
return &connectorv1.Account{
|
||||
Ref: &connectorv1.AccountRef{
|
||||
ConnectorId: ledgerConnectorID,
|
||||
AccountId: strings.TrimSpace(account.GetLedgerAccountRef()),
|
||||
},
|
||||
Kind: connectorv1.AccountKind_LEDGER_ACCOUNT,
|
||||
Asset: strings.TrimSpace(account.GetCurrency()),
|
||||
State: ledgerAccountState(account.GetStatus()),
|
||||
Label: strings.TrimSpace(account.GetAccountCode()),
|
||||
OwnerRef: strings.TrimSpace(account.GetOrganizationRef()),
|
||||
ProviderDetails: details,
|
||||
CreatedAt: account.GetCreatedAt(),
|
||||
UpdatedAt: account.GetUpdatedAt(),
|
||||
}
|
||||
}
|
||||
|
||||
func ledgerAccountState(status ledgerv1.AccountStatus) connectorv1.AccountState {
|
||||
switch status {
|
||||
case ledgerv1.AccountStatus_ACCOUNT_STATUS_ACTIVE:
|
||||
return connectorv1.AccountState_ACCOUNT_ACTIVE
|
||||
case ledgerv1.AccountStatus_ACCOUNT_STATUS_FROZEN:
|
||||
return connectorv1.AccountState_ACCOUNT_SUSPENDED
|
||||
default:
|
||||
return connectorv1.AccountState_ACCOUNT_STATE_UNSPECIFIED
|
||||
}
|
||||
}
|
||||
|
||||
func ledgerReceipt(ref string, status connectorv1.OperationStatus) *connectorv1.OperationReceipt {
|
||||
return &connectorv1.OperationReceipt{
|
||||
OperationId: strings.TrimSpace(ref),
|
||||
Status: status,
|
||||
ProviderRef: strings.TrimSpace(ref),
|
||||
}
|
||||
}
|
||||
|
||||
func ledgerEntryToOperation(entry *ledgerv1.JournalEntryResponse) *connectorv1.Operation {
|
||||
if entry == nil {
|
||||
return nil
|
||||
}
|
||||
op := &connectorv1.Operation{
|
||||
OperationId: strings.TrimSpace(entry.GetEntryRef()),
|
||||
Type: ledgerEntryType(entry.GetEntryType()),
|
||||
Status: connectorv1.OperationStatus_CONFIRMED,
|
||||
CreatedAt: entry.GetEventTime(),
|
||||
UpdatedAt: entry.GetEventTime(),
|
||||
}
|
||||
mainLines := ledgerMainLines(entry.GetLines())
|
||||
if len(mainLines) > 0 {
|
||||
op.Money = mainLines[0].GetMoney()
|
||||
}
|
||||
switch op.Type {
|
||||
case connectorv1.OperationType_CREDIT:
|
||||
if len(mainLines) > 0 {
|
||||
op.To = &connectorv1.OperationParty{Ref: &connectorv1.OperationParty_Account{Account: &connectorv1.AccountRef{ConnectorId: ledgerConnectorID, AccountId: mainLines[0].GetLedgerAccountRef()}}}
|
||||
}
|
||||
case connectorv1.OperationType_DEBIT:
|
||||
if len(mainLines) > 0 {
|
||||
op.From = &connectorv1.OperationParty{Ref: &connectorv1.OperationParty_Account{Account: &connectorv1.AccountRef{ConnectorId: ledgerConnectorID, AccountId: mainLines[0].GetLedgerAccountRef()}}}
|
||||
}
|
||||
case connectorv1.OperationType_TRANSFER, connectorv1.OperationType_FX:
|
||||
if len(mainLines) > 0 {
|
||||
op.From = &connectorv1.OperationParty{Ref: &connectorv1.OperationParty_Account{Account: &connectorv1.AccountRef{ConnectorId: ledgerConnectorID, AccountId: mainLines[0].GetLedgerAccountRef()}}}
|
||||
}
|
||||
if len(mainLines) > 1 {
|
||||
op.To = &connectorv1.OperationParty{Ref: &connectorv1.OperationParty_Account{Account: &connectorv1.AccountRef{ConnectorId: ledgerConnectorID, AccountId: mainLines[1].GetLedgerAccountRef()}}}
|
||||
}
|
||||
}
|
||||
return op
|
||||
}
|
||||
|
||||
func ledgerMainLines(lines []*ledgerv1.PostingLine) []*ledgerv1.PostingLine {
|
||||
if len(lines) == 0 {
|
||||
return nil
|
||||
}
|
||||
result := make([]*ledgerv1.PostingLine, 0, len(lines))
|
||||
for _, line := range lines {
|
||||
if line == nil {
|
||||
continue
|
||||
}
|
||||
if line.GetLineType() == ledgerv1.LineType_LINE_MAIN {
|
||||
result = append(result, line)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func ledgerEntryType(entryType ledgerv1.EntryType) connectorv1.OperationType {
|
||||
switch entryType {
|
||||
case ledgerv1.EntryType_ENTRY_CREDIT:
|
||||
return connectorv1.OperationType_CREDIT
|
||||
case ledgerv1.EntryType_ENTRY_DEBIT:
|
||||
return connectorv1.OperationType_DEBIT
|
||||
case ledgerv1.EntryType_ENTRY_TRANSFER:
|
||||
return connectorv1.OperationType_TRANSFER
|
||||
case ledgerv1.EntryType_ENTRY_FX:
|
||||
return connectorv1.OperationType_FX
|
||||
default:
|
||||
return connectorv1.OperationType_OPERATION_TYPE_UNSPECIFIED
|
||||
}
|
||||
}
|
||||
|
||||
func operationAccountID(party *connectorv1.OperationParty) string {
|
||||
if party == nil {
|
||||
return ""
|
||||
}
|
||||
if account := party.GetAccount(); account != nil {
|
||||
return strings.TrimSpace(account.GetAccountId())
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func parseLedgerAccountType(reader params.Reader, key string) (ledgerv1.AccountType, error) {
|
||||
value, ok := reader.Value(key)
|
||||
if !ok {
|
||||
return ledgerv1.AccountType_ACCOUNT_TYPE_UNSPECIFIED, fmt.Errorf("open_account: account_type is required")
|
||||
}
|
||||
switch v := value.(type) {
|
||||
case string:
|
||||
return parseLedgerAccountTypeString(v)
|
||||
case float64:
|
||||
return ledgerv1.AccountType(int32(v)), nil
|
||||
case int:
|
||||
return ledgerv1.AccountType(v), nil
|
||||
case int64:
|
||||
return ledgerv1.AccountType(v), nil
|
||||
default:
|
||||
return ledgerv1.AccountType_ACCOUNT_TYPE_UNSPECIFIED, fmt.Errorf("open_account: account_type is required")
|
||||
}
|
||||
}
|
||||
|
||||
func parseLedgerAccountTypeString(value string) (ledgerv1.AccountType, error) {
|
||||
switch strings.ToUpper(strings.TrimSpace(value)) {
|
||||
case "ACCOUNT_TYPE_ASSET", "ASSET":
|
||||
return ledgerv1.AccountType_ACCOUNT_TYPE_ASSET, nil
|
||||
case "ACCOUNT_TYPE_LIABILITY", "LIABILITY":
|
||||
return ledgerv1.AccountType_ACCOUNT_TYPE_LIABILITY, nil
|
||||
case "ACCOUNT_TYPE_REVENUE", "REVENUE":
|
||||
return ledgerv1.AccountType_ACCOUNT_TYPE_REVENUE, nil
|
||||
case "ACCOUNT_TYPE_EXPENSE", "EXPENSE":
|
||||
return ledgerv1.AccountType_ACCOUNT_TYPE_EXPENSE, nil
|
||||
default:
|
||||
return ledgerv1.AccountType_ACCOUNT_TYPE_UNSPECIFIED, fmt.Errorf("open_account: invalid account_type")
|
||||
}
|
||||
}
|
||||
|
||||
func parseLedgerAccountStatus(reader params.Reader, key string) ledgerv1.AccountStatus {
|
||||
value := strings.ToUpper(strings.TrimSpace(reader.String(key)))
|
||||
switch 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 parseEventTime(reader params.Reader) *timestamppb.Timestamp {
|
||||
raw := strings.TrimSpace(reader.String("event_time"))
|
||||
if raw == "" {
|
||||
return nil
|
||||
}
|
||||
parsed, err := time.Parse(time.RFC3339Nano, raw)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
return timestamppb.New(parsed)
|
||||
}
|
||||
|
||||
func parseLedgerCharges(reader params.Reader) ([]*ledgerv1.PostingLine, error) {
|
||||
items := reader.List("charges")
|
||||
if len(items) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
result := make([]*ledgerv1.PostingLine, 0, len(items))
|
||||
for i, item := range items {
|
||||
raw, ok := item.(map[string]interface{})
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("charges[%d]: invalid charge entry", i)
|
||||
}
|
||||
accountRef := strings.TrimSpace(fmt.Sprint(raw["ledger_account_ref"]))
|
||||
if accountRef == "" {
|
||||
return nil, fmt.Errorf("charges[%d]: ledger_account_ref is required", i)
|
||||
}
|
||||
money, err := parseMoneyFromMap(raw)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("charges[%d]: %w", i, err)
|
||||
}
|
||||
lineType := parseLedgerLineType(fmt.Sprint(raw["line_type"]))
|
||||
result = append(result, &ledgerv1.PostingLine{
|
||||
LedgerAccountRef: accountRef,
|
||||
Money: money,
|
||||
LineType: lineType,
|
||||
})
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func parseLedgerLineType(value string) ledgerv1.LineType {
|
||||
switch strings.ToUpper(strings.TrimSpace(value)) {
|
||||
case "LINE_TYPE_FEE", "FEE":
|
||||
return ledgerv1.LineType_LINE_FEE
|
||||
case "LINE_TYPE_SPREAD", "SPREAD":
|
||||
return ledgerv1.LineType_LINE_SPREAD
|
||||
case "LINE_TYPE_REVERSAL", "REVERSAL":
|
||||
return ledgerv1.LineType_LINE_REVERSAL
|
||||
default:
|
||||
return ledgerv1.LineType_LINE_FEE
|
||||
}
|
||||
}
|
||||
|
||||
func parseMoneyFromMap(raw map[string]interface{}) (*moneyv1.Money, error) {
|
||||
if raw == nil {
|
||||
return nil, fmt.Errorf("money is required")
|
||||
}
|
||||
amount := strings.TrimSpace(fmt.Sprint(raw["amount"]))
|
||||
currency := strings.TrimSpace(fmt.Sprint(raw["currency"]))
|
||||
if amount == "" || currency == "" {
|
||||
return nil, fmt.Errorf("money is required")
|
||||
}
|
||||
return &moneyv1.Money{
|
||||
Amount: amount,
|
||||
Currency: currency,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func mergeMetadata(base map[string]string, label, ownerRef, correlationID, parentIntentID string) map[string]string {
|
||||
metadata := map[string]string{}
|
||||
for k, v := range base {
|
||||
metadata[strings.TrimSpace(k)] = strings.TrimSpace(v)
|
||||
}
|
||||
if label != "" {
|
||||
if _, ok := metadata["label"]; !ok {
|
||||
metadata["label"] = label
|
||||
}
|
||||
}
|
||||
if ownerRef != "" {
|
||||
if _, ok := metadata["owner_ref"]; !ok {
|
||||
metadata["owner_ref"] = ownerRef
|
||||
}
|
||||
}
|
||||
if correlationID != "" {
|
||||
metadata["correlation_id"] = correlationID
|
||||
}
|
||||
if parentIntentID != "" {
|
||||
metadata["parent_intent_id"] = parentIntentID
|
||||
}
|
||||
if len(metadata) == 0 {
|
||||
return nil
|
||||
}
|
||||
return metadata
|
||||
}
|
||||
|
||||
func connectorError(code connectorv1.ErrorCode, message string, op *connectorv1.Operation, accountID string) *connectorv1.ConnectorError {
|
||||
err := &connectorv1.ConnectorError{
|
||||
Code: code,
|
||||
Message: strings.TrimSpace(message),
|
||||
AccountId: strings.TrimSpace(accountID),
|
||||
}
|
||||
if op != nil {
|
||||
err.CorrelationId = strings.TrimSpace(op.GetCorrelationId())
|
||||
err.ParentIntentId = strings.TrimSpace(op.GetParentIntentId())
|
||||
err.OperationId = strings.TrimSpace(op.GetOperationId())
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func mapErrorCode(err error) connectorv1.ErrorCode {
|
||||
switch {
|
||||
case errors.Is(err, merrors.ErrInvalidArg):
|
||||
return connectorv1.ErrorCode_INVALID_PARAMS
|
||||
case errors.Is(err, merrors.ErrNoData):
|
||||
return connectorv1.ErrorCode_NOT_FOUND
|
||||
case errors.Is(err, merrors.ErrNotImplemented):
|
||||
return connectorv1.ErrorCode_UNSUPPORTED_OPERATION
|
||||
case errors.Is(err, merrors.ErrInternal):
|
||||
return connectorv1.ErrorCode_TEMPORARY_UNAVAILABLE
|
||||
default:
|
||||
return connectorv1.ErrorCode_PROVIDER_ERROR
|
||||
}
|
||||
}
|
||||
@@ -24,7 +24,7 @@ import (
|
||||
pmessaging "github.com/tech/sendico/pkg/messaging"
|
||||
"github.com/tech/sendico/pkg/mlogger"
|
||||
"github.com/tech/sendico/pkg/mservice"
|
||||
unifiedv1 "github.com/tech/sendico/pkg/proto/gateway/unified/v1"
|
||||
connectorv1 "github.com/tech/sendico/pkg/proto/connector/v1"
|
||||
ledgerv1 "github.com/tech/sendico/pkg/proto/ledger/v1"
|
||||
)
|
||||
|
||||
@@ -50,7 +50,6 @@ type Service struct {
|
||||
cancel context.CancelFunc
|
||||
publisher *outboxPublisher
|
||||
}
|
||||
unifiedv1.UnimplementedUnifiedGatewayServiceServer
|
||||
}
|
||||
|
||||
type feesDependency struct {
|
||||
@@ -83,7 +82,7 @@ func NewService(logger mlogger.Logger, repo storage.Repository, prod pmessaging.
|
||||
|
||||
func (s *Service) Register(router routers.GRPC) error {
|
||||
return router.Register(func(reg grpc.ServiceRegistrar) {
|
||||
unifiedv1.RegisterUnifiedGatewayServiceServer(reg, s)
|
||||
connectorv1.RegisterConnectorServiceServer(reg, newConnectorAdapter(s))
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user