Orchestration / payments v2 #554
@@ -392,7 +392,7 @@ func (s *Service) startDiscoveryAnnouncer() {
|
||||
|
||||
announce := discovery.Announcement{
|
||||
Service: "BILLING_DOCUMENTS",
|
||||
Operations: []string{"documents.batch_resolve", "documents.get"},
|
||||
Operations: []string{discovery.OperationDocumentsBatchResolve, discovery.OperationDocumentsGet},
|
||||
InvokeURI: s.invokeURI,
|
||||
Version: appversion.Create().Short(),
|
||||
}
|
||||
|
||||
@@ -564,7 +564,7 @@ func (s *Service) startDiscoveryAnnouncer() {
|
||||
|
||||
announce := discovery.Announcement{
|
||||
Service: "BILLING_FEES",
|
||||
Operations: []string{"fee.calc"},
|
||||
Operations: []string{discovery.OperationFeeCalc},
|
||||
InvokeURI: s.invokeURI,
|
||||
Version: appversion.Create().Short(),
|
||||
}
|
||||
|
||||
@@ -51,7 +51,7 @@ func (i *Imp) startDiscovery(cfg *config) error {
|
||||
announce := discovery.Announcement{
|
||||
Service: "DISCOVERY",
|
||||
InstanceID: discovery.InstanceID(),
|
||||
Operations: []string{"discovery.lookup"},
|
||||
Operations: []string{discovery.OperationDiscoveryLookup},
|
||||
Version: appversion.Create().Short(),
|
||||
}
|
||||
i.announcer = discovery.NewAnnouncer(i.logger, producer, mservice.Discovery, announce)
|
||||
|
||||
@@ -86,7 +86,7 @@ func (a *App) Run(ctx context.Context) error {
|
||||
producer := msgproducer.NewProducer(a.logger.Named("discovery_producer"), broker)
|
||||
announce := discovery.Announcement{
|
||||
Service: "FX_INGESTOR",
|
||||
Operations: []string{"fx.ingest"},
|
||||
Operations: []string{discovery.OperationFXIngest},
|
||||
Version: appversion.Create().Short(),
|
||||
}
|
||||
announcer = discovery.NewAnnouncer(a.logger, producer, "fx_ingestor", announce)
|
||||
|
||||
@@ -106,7 +106,7 @@ func (s *Service) startDiscoveryAnnouncer() {
|
||||
}
|
||||
announce := discovery.Announcement{
|
||||
Service: "FX_ORACLE",
|
||||
Operations: []string{"fx.quote"},
|
||||
Operations: []string{discovery.OperationFXQuote},
|
||||
InvokeURI: s.invokeURI,
|
||||
Version: appversion.Create().Short(),
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/tech/sendico/pkg/discovery"
|
||||
"github.com/tech/sendico/pkg/ledgerconv"
|
||||
"github.com/tech/sendico/pkg/merrors"
|
||||
"github.com/tech/sendico/pkg/model/account_role"
|
||||
@@ -23,7 +24,36 @@ import (
|
||||
"google.golang.org/protobuf/types/known/timestamppb"
|
||||
)
|
||||
|
||||
const ledgerConnectorID = "ledger"
|
||||
const (
|
||||
ledgerConnectorID = "ledger"
|
||||
ledgerRailName = "LEDGER"
|
||||
|
||||
opParamOperation = "operation"
|
||||
opParamToMoney = "to_money"
|
||||
opParamAmount = "amount"
|
||||
opParamCurrency = "currency"
|
||||
opParamOrganizationRef = "organization_ref"
|
||||
opParamAccountType = "account_type"
|
||||
opParamStatus = "status"
|
||||
opParamAllowNegative = "allow_negative"
|
||||
opParamRole = "role"
|
||||
opParamDescription = "description"
|
||||
opParamMetadata = "metadata"
|
||||
opParamCharges = "charges"
|
||||
opParamEventTime = "event_time"
|
||||
opParamContraLedgerAccountRef = "contra_ledger_account_ref"
|
||||
opParamLedgerAccountRef = "ledger_account_ref"
|
||||
opParamLineType = "line_type"
|
||||
opParamAccountCode = "account_code"
|
||||
opParamIsSettlement = "is_settlement"
|
||||
|
||||
txMetaPaymentPlanID = "payment_plan_id"
|
||||
txMetaFromRail = "from_rail"
|
||||
txMetaToRail = "to_rail"
|
||||
txMetaExternalReference = "external_reference_id"
|
||||
txMetaFXRateUsed = "fx_rate_used"
|
||||
txMetaFeeAmount = "fee_amount"
|
||||
)
|
||||
|
||||
// Client exposes typed helpers around the ledger gRPC API.
|
||||
type Client interface {
|
||||
@@ -36,6 +66,8 @@ type Client interface {
|
||||
ListConnectorAccounts(ctx context.Context, req *connectorv1.ListAccountsRequest) (*connectorv1.ListAccountsResponse, error)
|
||||
PostCreditWithCharges(ctx context.Context, req *ledgerv1.PostCreditRequest) (*ledgerv1.PostResponse, error)
|
||||
PostDebitWithCharges(ctx context.Context, req *ledgerv1.PostDebitRequest) (*ledgerv1.PostResponse, error)
|
||||
PostExternalCreditWithCharges(ctx context.Context, req *ledgerv1.PostCreditRequest) (*ledgerv1.PostResponse, error)
|
||||
PostExternalDebitWithCharges(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)
|
||||
|
||||
@@ -148,7 +180,7 @@ func (c *ledgerClient) CreateTransaction(ctx context.Context, tx rail.LedgerTx)
|
||||
metadata := ledgerTxMetadata(tx.Metadata, tx)
|
||||
extraParams := map[string]interface{}{}
|
||||
if op := strings.TrimSpace(tx.Operation); op != "" {
|
||||
extraParams["operation"] = op
|
||||
extraParams[opParamOperation] = op
|
||||
}
|
||||
if len(extraParams) == 0 {
|
||||
extraParams = nil
|
||||
@@ -204,13 +236,13 @@ func (c *ledgerClient) CreateAccount(ctx context.Context, req *ledgerv1.CreateAc
|
||||
return nil, merrors.InvalidArgument("ledger: currency is required")
|
||||
}
|
||||
params := map[string]interface{}{
|
||||
"organization_ref": strings.TrimSpace(req.GetOrganizationRef()),
|
||||
"account_type": req.GetAccountType().String(),
|
||||
"status": req.GetStatus().String(),
|
||||
"allow_negative": req.GetAllowNegative(),
|
||||
opParamOrganizationRef: strings.TrimSpace(req.GetOrganizationRef()),
|
||||
opParamAccountType: req.GetAccountType().String(),
|
||||
opParamStatus: req.GetStatus().String(),
|
||||
opParamAllowNegative: req.GetAllowNegative(),
|
||||
}
|
||||
if role := req.GetRole(); role != ledgerv1.AccountRole_ACCOUNT_ROLE_UNSPECIFIED {
|
||||
params["role"] = role.String()
|
||||
params[opParamRole] = role.String()
|
||||
}
|
||||
label := ""
|
||||
if desc := req.GetDescribable(); desc != nil {
|
||||
@@ -218,12 +250,12 @@ func (c *ledgerClient) CreateAccount(ctx context.Context, req *ledgerv1.CreateAc
|
||||
if desc.Description != nil {
|
||||
trimmed := strings.TrimSpace(desc.GetDescription())
|
||||
if trimmed != "" {
|
||||
params["description"] = trimmed
|
||||
params[opParamDescription] = trimmed
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(req.GetMetadata()) > 0 {
|
||||
params["metadata"] = mapStringToInterface(req.GetMetadata())
|
||||
params[opParamMetadata] = mapStringToInterface(req.GetMetadata())
|
||||
}
|
||||
resp, err := c.client.OpenAccount(ctx, &connectorv1.OpenAccountRequest{
|
||||
Kind: connectorv1.AccountKind_LEDGER_ACCOUNT,
|
||||
@@ -277,6 +309,30 @@ func (c *ledgerClient) PostDebitWithCharges(ctx context.Context, req *ledgerv1.P
|
||||
return c.submitLedgerOperation(ctx, connectorv1.OperationType_DEBIT, req.GetLedgerAccountRef(), "", req.GetMoney(), req)
|
||||
}
|
||||
|
||||
func (c *ledgerClient) PostExternalCreditWithCharges(ctx context.Context, req *ledgerv1.PostCreditRequest) (*ledgerv1.PostResponse, error) {
|
||||
return c.submitLedgerOperationWithExtras(
|
||||
ctx,
|
||||
connectorv1.OperationType_CREDIT,
|
||||
"",
|
||||
req.GetLedgerAccountRef(),
|
||||
req.GetMoney(),
|
||||
req,
|
||||
map[string]interface{}{opParamOperation: discovery.OperationExternalCredit},
|
||||
)
|
||||
}
|
||||
|
||||
func (c *ledgerClient) PostExternalDebitWithCharges(ctx context.Context, req *ledgerv1.PostDebitRequest) (*ledgerv1.PostResponse, error) {
|
||||
return c.submitLedgerOperationWithExtras(
|
||||
ctx,
|
||||
connectorv1.OperationType_DEBIT,
|
||||
req.GetLedgerAccountRef(),
|
||||
"",
|
||||
req.GetMoney(),
|
||||
req,
|
||||
map[string]interface{}{opParamOperation: discovery.OperationExternalDebit},
|
||||
)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
@@ -292,7 +348,7 @@ func (c *ledgerClient) ApplyFXWithCharges(ctx context.Context, req *ledgerv1.FXR
|
||||
}
|
||||
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()}
|
||||
params[opParamToMoney] = map[string]interface{}{opParamAmount: req.GetToMoney().GetAmount(), opParamCurrency: req.GetToMoney().GetCurrency()}
|
||||
operation := &connectorv1.Operation{
|
||||
Type: connectorv1.OperationType_FX,
|
||||
IdempotencyKey: strings.TrimSpace(req.GetIdempotencyKey()),
|
||||
@@ -466,7 +522,7 @@ func (c *ledgerClient) submitLedgerOperationWithExtras(ctx context.Context, opTy
|
||||
|
||||
params := ledgerOperationParams(orgRef, description, metadata, charges, eventTime)
|
||||
if contraRef != "" {
|
||||
params["contra_ledger_account_ref"] = strings.TrimSpace(contraRef)
|
||||
params[opParamContraLedgerAccountRef] = strings.TrimSpace(contraRef)
|
||||
}
|
||||
if len(extraParams) > 0 {
|
||||
for key, value := range extraParams {
|
||||
@@ -534,17 +590,17 @@ func accountRoleFromLedgerProto(role ledgerv1.AccountRole) account_role.AccountR
|
||||
|
||||
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),
|
||||
opParamOrganizationRef: strings.TrimSpace(orgRef),
|
||||
opParamDescription: strings.TrimSpace(description),
|
||||
}
|
||||
if len(metadata) > 0 {
|
||||
params["metadata"] = mapStringToInterface(metadata)
|
||||
params[opParamMetadata] = mapStringToInterface(metadata)
|
||||
}
|
||||
if len(charges) > 0 {
|
||||
params["charges"] = chargesToInterface(charges)
|
||||
params[opParamCharges] = chargesToInterface(charges)
|
||||
}
|
||||
if eventTime != nil {
|
||||
params["event_time"] = eventTime.AsTime().UTC().Format(time.RFC3339Nano)
|
||||
params[opParamEventTime] = eventTime.AsTime().UTC().Format(time.RFC3339Nano)
|
||||
}
|
||||
return params
|
||||
}
|
||||
@@ -580,25 +636,25 @@ func ledgerAccountFromConnector(account *connectorv1.Account) *ledgerv1.LedgerAc
|
||||
details = account.GetProviderDetails().AsMap()
|
||||
}
|
||||
accountType := ledgerv1.AccountType_ACCOUNT_TYPE_UNSPECIFIED
|
||||
if v := strings.TrimSpace(fmt.Sprint(details["account_type"])); v != "" {
|
||||
if v := strings.TrimSpace(fmt.Sprint(details[opParamAccountType])); v != "" {
|
||||
accountType = parseAccountType(v)
|
||||
}
|
||||
status := ledgerv1.AccountStatus_ACCOUNT_STATUS_UNSPECIFIED
|
||||
if v := strings.TrimSpace(fmt.Sprint(details["status"])); v != "" {
|
||||
if v := strings.TrimSpace(fmt.Sprint(details[opParamStatus])); v != "" {
|
||||
status = parseAccountStatus(v)
|
||||
}
|
||||
allowNegative := false
|
||||
if v, ok := details["allow_negative"].(bool); ok {
|
||||
if v, ok := details[opParamAllowNegative].(bool); ok {
|
||||
allowNegative = v
|
||||
}
|
||||
role := ledgerv1.AccountRole_ACCOUNT_ROLE_UNSPECIFIED
|
||||
if v := strings.TrimSpace(fmt.Sprint(details["role"])); v != "" {
|
||||
if v := strings.TrimSpace(fmt.Sprint(details[opParamRole])); v != "" {
|
||||
if parsed, ok := ledgerconv.ParseAccountRole(v); ok {
|
||||
role = parsed
|
||||
}
|
||||
}
|
||||
if role == ledgerv1.AccountRole_ACCOUNT_ROLE_UNSPECIFIED {
|
||||
switch v := details["is_settlement"].(type) {
|
||||
switch v := details[opParamIsSettlement].(type) {
|
||||
case bool:
|
||||
if v {
|
||||
role = ledgerv1.AccountRole_ACCOUNT_ROLE_SETTLEMENT
|
||||
@@ -609,13 +665,13 @@ func ledgerAccountFromConnector(account *connectorv1.Account) *ledgerv1.LedgerAc
|
||||
}
|
||||
}
|
||||
}
|
||||
accountCode := strings.TrimSpace(fmt.Sprint(details["account_code"]))
|
||||
accountCode := strings.TrimSpace(fmt.Sprint(details[opParamAccountCode]))
|
||||
accountID := ""
|
||||
if ref := account.GetRef(); ref != nil {
|
||||
accountID = strings.TrimSpace(ref.GetAccountId())
|
||||
}
|
||||
organizationRef := strings.TrimSpace(account.GetOwnerRef())
|
||||
if v := strings.TrimSpace(fmt.Sprint(details["organization_ref"])); v != "" {
|
||||
if v := strings.TrimSpace(fmt.Sprint(details[opParamOrganizationRef])); v != "" {
|
||||
organizationRef = v
|
||||
}
|
||||
describable := account.GetDescribable()
|
||||
@@ -674,7 +730,7 @@ func operationDescription(op *connectorv1.Operation) string {
|
||||
if op == nil || op.GetParams() == nil {
|
||||
return ""
|
||||
}
|
||||
if value, ok := op.GetParams().AsMap()["description"]; ok {
|
||||
if value, ok := op.GetParams().AsMap()[opParamDescription]; ok {
|
||||
return strings.TrimSpace(fmt.Sprint(value))
|
||||
}
|
||||
return ""
|
||||
@@ -731,10 +787,10 @@ func chargesToInterface(charges []*ledgerv1.PostingLine) []interface{} {
|
||||
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(),
|
||||
opParamLedgerAccountRef: strings.TrimSpace(line.GetLedgerAccountRef()),
|
||||
opParamAmount: strings.TrimSpace(line.GetMoney().GetAmount()),
|
||||
opParamCurrency: strings.TrimSpace(line.GetMoney().GetCurrency()),
|
||||
opParamLineType: line.GetLineType().String(),
|
||||
})
|
||||
}
|
||||
if len(result) == 0 {
|
||||
@@ -793,7 +849,7 @@ func (c *ledgerClient) callContext(ctx context.Context) (context.Context, contex
|
||||
}
|
||||
|
||||
func isLedgerRail(value string) bool {
|
||||
return strings.EqualFold(strings.TrimSpace(value), "LEDGER")
|
||||
return strings.EqualFold(strings.TrimSpace(value), ledgerRailName)
|
||||
}
|
||||
|
||||
func cloneMoney(input *moneyv1.Money) *moneyv1.Money {
|
||||
@@ -823,22 +879,22 @@ func ledgerTxMetadata(base map[string]string, tx rail.LedgerTx) map[string]strin
|
||||
meta = map[string]string{}
|
||||
}
|
||||
if val := strings.TrimSpace(tx.PaymentPlanID); val != "" {
|
||||
meta["payment_plan_id"] = val
|
||||
meta[txMetaPaymentPlanID] = val
|
||||
}
|
||||
if val := strings.TrimSpace(tx.FromRail); val != "" {
|
||||
meta["from_rail"] = val
|
||||
meta[txMetaFromRail] = val
|
||||
}
|
||||
if val := strings.TrimSpace(tx.ToRail); val != "" {
|
||||
meta["to_rail"] = val
|
||||
meta[txMetaToRail] = val
|
||||
}
|
||||
if val := strings.TrimSpace(tx.ExternalReferenceID); val != "" {
|
||||
meta["external_reference_id"] = val
|
||||
meta[txMetaExternalReference] = val
|
||||
}
|
||||
if val := strings.TrimSpace(tx.FXRateUsed); val != "" {
|
||||
meta["fx_rate_used"] = val
|
||||
meta[txMetaFXRateUsed] = val
|
||||
}
|
||||
if val := strings.TrimSpace(tx.FeeAmount); val != "" {
|
||||
meta["fee_amount"] = val
|
||||
meta[txMetaFeeAmount] = val
|
||||
}
|
||||
if len(meta) == 0 {
|
||||
return nil
|
||||
|
||||
@@ -6,6 +6,8 @@ import (
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/tech/sendico/pkg/discovery"
|
||||
accountrolev1 "github.com/tech/sendico/pkg/proto/common/account_role/v1"
|
||||
moneyv1 "github.com/tech/sendico/pkg/proto/common/money/v1"
|
||||
connectorv1 "github.com/tech/sendico/pkg/proto/connector/v1"
|
||||
ledgerv1 "github.com/tech/sendico/pkg/proto/ledger/v1"
|
||||
@@ -92,3 +94,65 @@ func TestTransferInternal_SubmitsTransferOperation(t *testing.T) {
|
||||
assert.Equal(t, "op-1", resp.GetJournalEntryRef())
|
||||
assert.Equal(t, ledgerv1.EntryType_ENTRY_TRANSFER, resp.GetEntryType())
|
||||
}
|
||||
|
||||
func TestPostExternalCreditWithCharges_SubmitsExternalOperation(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
var captured *connectorv1.Operation
|
||||
stub := &stubConnector{
|
||||
submitFn: func(ctx context.Context, req *connectorv1.SubmitOperationRequest) (*connectorv1.SubmitOperationResponse, error) {
|
||||
captured = req.GetOperation()
|
||||
return &connectorv1.SubmitOperationResponse{Receipt: &connectorv1.OperationReceipt{OperationId: "op-ext-credit"}}, nil
|
||||
},
|
||||
}
|
||||
|
||||
client := NewWithClient(Config{}, stub)
|
||||
resp, err := client.PostExternalCreditWithCharges(ctx, &ledgerv1.PostCreditRequest{
|
||||
IdempotencyKey: "id-ext-credit",
|
||||
OrganizationRef: "org-1",
|
||||
Money: &moneyv1.Money{Currency: "USDT", Amount: "1.0"},
|
||||
Role: ledgerv1.AccountRole_ACCOUNT_ROLE_OPERATING,
|
||||
})
|
||||
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, resp)
|
||||
require.NotNil(t, captured)
|
||||
|
||||
assert.Equal(t, connectorv1.OperationType_CREDIT, captured.GetType())
|
||||
assert.Equal(t, "", captured.GetTo().GetAccount().GetAccountId())
|
||||
assert.Equal(t, accountrolev1.AccountRole_OPERATING, captured.GetToRole())
|
||||
assert.Equal(t, discovery.OperationExternalCredit, captured.GetParams().AsMap()["operation"])
|
||||
assert.Equal(t, "op-ext-credit", resp.GetJournalEntryRef())
|
||||
assert.Equal(t, ledgerv1.EntryType_ENTRY_CREDIT, resp.GetEntryType())
|
||||
}
|
||||
|
||||
func TestPostExternalDebitWithCharges_SubmitsExternalOperation(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
var captured *connectorv1.Operation
|
||||
stub := &stubConnector{
|
||||
submitFn: func(ctx context.Context, req *connectorv1.SubmitOperationRequest) (*connectorv1.SubmitOperationResponse, error) {
|
||||
captured = req.GetOperation()
|
||||
return &connectorv1.SubmitOperationResponse{Receipt: &connectorv1.OperationReceipt{OperationId: "op-ext-debit"}}, nil
|
||||
},
|
||||
}
|
||||
|
||||
client := NewWithClient(Config{}, stub)
|
||||
resp, err := client.PostExternalDebitWithCharges(ctx, &ledgerv1.PostDebitRequest{
|
||||
IdempotencyKey: "id-ext-debit",
|
||||
OrganizationRef: "org-1",
|
||||
Money: &moneyv1.Money{Currency: "RUB", Amount: "77.14"},
|
||||
Role: ledgerv1.AccountRole_ACCOUNT_ROLE_HOLD,
|
||||
})
|
||||
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, resp)
|
||||
require.NotNil(t, captured)
|
||||
|
||||
assert.Equal(t, connectorv1.OperationType_DEBIT, captured.GetType())
|
||||
assert.Equal(t, "", captured.GetFrom().GetAccount().GetAccountId())
|
||||
assert.Equal(t, accountrolev1.AccountRole_HOLD, captured.GetFromRole())
|
||||
assert.Equal(t, discovery.OperationExternalDebit, captured.GetParams().AsMap()["operation"])
|
||||
assert.Equal(t, "op-ext-debit", resp.GetJournalEntryRef())
|
||||
assert.Equal(t, ledgerv1.EntryType_ENTRY_DEBIT, resp.GetEntryType())
|
||||
}
|
||||
|
||||
@@ -4,29 +4,31 @@ import (
|
||||
"context"
|
||||
|
||||
"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"
|
||||
connectorv1 "github.com/tech/sendico/pkg/proto/connector/v1"
|
||||
ledgerv1 "github.com/tech/sendico/pkg/proto/ledger/v1"
|
||||
)
|
||||
|
||||
// Fake implements Client for tests.
|
||||
type Fake struct {
|
||||
ReadBalanceFn func(ctx context.Context, accountID string) (*moneyv1.Money, error)
|
||||
CreateTransactionFn func(ctx context.Context, tx rail.LedgerTx) (string, error)
|
||||
HoldBalanceFn func(ctx context.Context, accountID string, amount string) error
|
||||
CreateAccountFn func(ctx context.Context, req *ledgerv1.CreateAccountRequest) (*ledgerv1.CreateAccountResponse, error)
|
||||
ListAccountsFn func(ctx context.Context, req *ledgerv1.ListAccountsRequest) (*ledgerv1.ListAccountsResponse, error)
|
||||
ListConnectorAccountsFn func(ctx context.Context, req *connectorv1.ListAccountsRequest) (*connectorv1.ListAccountsResponse, error)
|
||||
PostCreditWithChargesFn func(ctx context.Context, req *ledgerv1.PostCreditRequest) (*ledgerv1.PostResponse, error)
|
||||
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)
|
||||
CloseFn func() error
|
||||
ReadBalanceFn func(ctx context.Context, accountID string) (*moneyv1.Money, error)
|
||||
CreateTransactionFn func(ctx context.Context, tx rail.LedgerTx) (string, error)
|
||||
HoldBalanceFn func(ctx context.Context, accountID string, amount string) error
|
||||
CreateAccountFn func(ctx context.Context, req *ledgerv1.CreateAccountRequest) (*ledgerv1.CreateAccountResponse, error)
|
||||
ListAccountsFn func(ctx context.Context, req *ledgerv1.ListAccountsRequest) (*ledgerv1.ListAccountsResponse, error)
|
||||
ListConnectorAccountsFn func(ctx context.Context, req *connectorv1.ListAccountsRequest) (*connectorv1.ListAccountsResponse, error)
|
||||
PostCreditWithChargesFn func(ctx context.Context, req *ledgerv1.PostCreditRequest) (*ledgerv1.PostResponse, error)
|
||||
PostDebitWithChargesFn func(ctx context.Context, req *ledgerv1.PostDebitRequest) (*ledgerv1.PostResponse, error)
|
||||
PostExternalCreditWithChargesFn func(ctx context.Context, req *ledgerv1.PostCreditRequest) (*ledgerv1.PostResponse, error)
|
||||
PostExternalDebitWithChargesFn 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)
|
||||
CloseFn func() error
|
||||
}
|
||||
|
||||
func (f *Fake) ReadBalance(ctx context.Context, accountID string) (*moneyv1.Money, error) {
|
||||
@@ -85,6 +87,20 @@ func (f *Fake) PostDebitWithCharges(ctx context.Context, req *ledgerv1.PostDebit
|
||||
return &ledgerv1.PostResponse{}, nil
|
||||
}
|
||||
|
||||
func (f *Fake) PostExternalCreditWithCharges(ctx context.Context, req *ledgerv1.PostCreditRequest) (*ledgerv1.PostResponse, error) {
|
||||
if f.PostExternalCreditWithChargesFn != nil {
|
||||
return f.PostExternalCreditWithChargesFn(ctx, req)
|
||||
}
|
||||
return &ledgerv1.PostResponse{}, nil
|
||||
}
|
||||
|
||||
func (f *Fake) PostExternalDebitWithCharges(ctx context.Context, req *ledgerv1.PostDebitRequest) (*ledgerv1.PostResponse, error) {
|
||||
if f.PostExternalDebitWithChargesFn != nil {
|
||||
return f.PostExternalDebitWithChargesFn(ctx, req)
|
||||
}
|
||||
return &ledgerv1.PostResponse{}, nil
|
||||
}
|
||||
|
||||
func (f *Fake) TransferInternal(ctx context.Context, req *ledgerv1.TransferRequest) (*ledgerv1.PostResponse, error) {
|
||||
if f.TransferInternalFn != nil {
|
||||
return f.TransferInternalFn(ctx, req)
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
|
||||
"github.com/tech/sendico/ledger/internal/appversion"
|
||||
"github.com/tech/sendico/pkg/connector/params"
|
||||
"github.com/tech/sendico/pkg/discovery"
|
||||
"github.com/tech/sendico/pkg/ledgerconv"
|
||||
"github.com/tech/sendico/pkg/merrors"
|
||||
accountrolev1 "github.com/tech/sendico/pkg/proto/common/account_role/v1"
|
||||
@@ -16,6 +17,7 @@ import (
|
||||
moneyv1 "github.com/tech/sendico/pkg/proto/common/money/v1"
|
||||
connectorv1 "github.com/tech/sendico/pkg/proto/connector/v1"
|
||||
ledgerv1 "github.com/tech/sendico/pkg/proto/ledger/v1"
|
||||
"go.uber.org/zap"
|
||||
"google.golang.org/protobuf/types/known/structpb"
|
||||
"google.golang.org/protobuf/types/known/timestamppb"
|
||||
)
|
||||
@@ -222,7 +224,7 @@ func (c *connectorAdapter) SubmitOperation(ctx context.Context, req *connectorv1
|
||||
if err != nil {
|
||||
return &connectorv1.SubmitOperationResponse{Receipt: &connectorv1.OperationReceipt{Error: connectorError(connectorv1.ErrorCode_INVALID_PARAMS, err.Error(), op, "")}}, nil
|
||||
}
|
||||
operation := strings.ToLower(strings.TrimSpace(reader.String("operation")))
|
||||
operation := discovery.NormalizeOperation(reader.String("operation"))
|
||||
|
||||
switch op.GetType() {
|
||||
case connectorv1.OperationType_CREDIT:
|
||||
@@ -230,11 +232,11 @@ func (c *connectorAdapter) SubmitOperation(ctx context.Context, req *connectorv1
|
||||
if accountID == "" && op.GetToRole() == accountrolev1.AccountRole_ACCOUNT_ROLE_UNSPECIFIED {
|
||||
return &connectorv1.SubmitOperationResponse{Receipt: &connectorv1.OperationReceipt{Error: connectorError(connectorv1.ErrorCode_INVALID_PARAMS, "credit: to.account or to_role is required", op, "")}}, nil
|
||||
}
|
||||
if operation != "" && operation != "external.credit" {
|
||||
if operation != "" && operation != discovery.OperationExternalCredit {
|
||||
return &connectorv1.SubmitOperationResponse{Receipt: &connectorv1.OperationReceipt{Error: connectorError(connectorv1.ErrorCode_INVALID_PARAMS, "credit: unsupported operation override", op, "")}}, nil
|
||||
}
|
||||
creditFn := c.svc.PostCreditWithCharges
|
||||
if operation == "external.credit" {
|
||||
if operation == discovery.OperationExternalCredit {
|
||||
creditFn = c.svc.PostExternalCreditWithCharges
|
||||
}
|
||||
resp, err := creditFn(ctx, &ledgerv1.PostCreditRequest{
|
||||
@@ -250,6 +252,10 @@ func (c *connectorAdapter) SubmitOperation(ctx context.Context, req *connectorv1
|
||||
Role: accountRoleFromConnectorRole(op.GetToRole()),
|
||||
})
|
||||
if err != nil {
|
||||
c.svc.logger.Warn("Operation failed", zap.Error(err), zap.String("operation", operation),
|
||||
zap.String("idempotency_key", op.IdempotencyKey), zap.String("description", description),
|
||||
zap.String("organization_ref", orgRef), zap.String("ledger_account_ref", accountID),
|
||||
)
|
||||
return &connectorv1.SubmitOperationResponse{Receipt: &connectorv1.OperationReceipt{Error: connectorError(mapErrorCode(err), err.Error(), op, accountID)}}, nil
|
||||
}
|
||||
return &connectorv1.SubmitOperationResponse{Receipt: ledgerReceipt(resp.GetJournalEntryRef(), connectorv1.OperationStatus_OPERATION_SUCCESS)}, nil
|
||||
@@ -258,11 +264,11 @@ func (c *connectorAdapter) SubmitOperation(ctx context.Context, req *connectorv1
|
||||
if accountID == "" && op.GetFromRole() == accountrolev1.AccountRole_ACCOUNT_ROLE_UNSPECIFIED {
|
||||
return &connectorv1.SubmitOperationResponse{Receipt: &connectorv1.OperationReceipt{Error: connectorError(connectorv1.ErrorCode_INVALID_PARAMS, "debit: from.account or from_role is required", op, "")}}, nil
|
||||
}
|
||||
if operation != "" && operation != "external.debit" {
|
||||
if operation != "" && operation != discovery.OperationExternalDebit {
|
||||
return &connectorv1.SubmitOperationResponse{Receipt: &connectorv1.OperationReceipt{Error: connectorError(connectorv1.ErrorCode_INVALID_PARAMS, "debit: unsupported operation override", op, "")}}, nil
|
||||
}
|
||||
debitFn := c.svc.PostDebitWithCharges
|
||||
if operation == "external.debit" {
|
||||
if operation == discovery.OperationExternalDebit {
|
||||
debitFn = c.svc.PostExternalDebitWithCharges
|
||||
}
|
||||
resp, err := debitFn(ctx, &ledgerv1.PostDebitRequest{
|
||||
@@ -393,14 +399,14 @@ func ledgerOperationParams() []*connectorv1.OperationParamSpec {
|
||||
Type: connectorv1.ParamType_STRING,
|
||||
Required: false,
|
||||
Description: "Optional ledger operation override (external.credit).",
|
||||
AllowedValues: []string{"external.credit"},
|
||||
AllowedValues: []string{discovery.OperationExternalCredit},
|
||||
}
|
||||
externalDebit := &connectorv1.ParamSpec{
|
||||
Key: "operation",
|
||||
Type: connectorv1.ParamType_STRING,
|
||||
Required: false,
|
||||
Description: "Optional ledger operation override (external.debit).",
|
||||
AllowedValues: []string{"external.debit"},
|
||||
AllowedValues: []string{discovery.OperationExternalDebit},
|
||||
}
|
||||
return []*connectorv1.OperationParamSpec{
|
||||
{OperationType: connectorv1.OperationType_CREDIT, Params: append(common, externalCredit, &connectorv1.ParamSpec{Key: "contra_ledger_account_ref", Type: connectorv1.ParamType_STRING, Required: false})},
|
||||
|
||||
@@ -7,6 +7,40 @@ import (
|
||||
"github.com/prometheus/client_golang/prometheus/promauto"
|
||||
)
|
||||
|
||||
type journalEntryType string
|
||||
|
||||
const (
|
||||
journalEntryTypeCredit journalEntryType = "credit"
|
||||
journalEntryTypeDebit journalEntryType = "debit"
|
||||
journalEntryTypeTransfer journalEntryType = "transfer"
|
||||
journalEntryTypeFX journalEntryType = "fx"
|
||||
)
|
||||
|
||||
type journalEntryStatus string
|
||||
|
||||
const (
|
||||
journalEntryStatusAttempted journalEntryStatus = "attempted"
|
||||
journalEntryStatusSuccess journalEntryStatus = "success"
|
||||
journalEntryStatusError journalEntryStatus = "error"
|
||||
)
|
||||
|
||||
type journalEntryErrorType string
|
||||
|
||||
const (
|
||||
journalEntryErrorNotImplemented journalEntryErrorType = "not_implemented"
|
||||
journalEntryErrorFailed journalEntryErrorType = "failed"
|
||||
journalEntryErrorIdempotencyCheck journalEntryErrorType = "idempotency_check_failed"
|
||||
journalEntryErrorAccountResolve journalEntryErrorType = "account_resolve_failed"
|
||||
journalEntryErrorAccountInvalid journalEntryErrorType = "account_invalid"
|
||||
journalEntryErrorContraResolve journalEntryErrorType = "contra_resolve_failed"
|
||||
journalEntryErrorContraMissingID journalEntryErrorType = "contra_missing_id"
|
||||
journalEntryErrorSystemAccountResolve journalEntryErrorType = "system_account_resolve_failed"
|
||||
journalEntryErrorSystemAccountInvalid journalEntryErrorType = "system_account_invalid"
|
||||
journalEntryErrorSystemAccountMissing journalEntryErrorType = "system_account_missing_id"
|
||||
journalEntryErrorUnbalancedAfterContra journalEntryErrorType = "unbalanced_after_contra"
|
||||
journalEntryErrorTransactionFailed journalEntryErrorType = "transaction_failed"
|
||||
)
|
||||
|
||||
var (
|
||||
metricsOnce sync.Once
|
||||
|
||||
@@ -110,16 +144,16 @@ func initMetrics() {
|
||||
|
||||
// Metric recording helpers
|
||||
|
||||
func recordJournalEntry(entryType, status string, durationSeconds float64) {
|
||||
func recordJournalEntry(entryType journalEntryType, status journalEntryStatus, durationSeconds float64) {
|
||||
initMetrics()
|
||||
journalEntriesTotal.WithLabelValues(entryType, status).Inc()
|
||||
journalEntryLatency.WithLabelValues(entryType).Observe(durationSeconds)
|
||||
journalEntriesTotal.WithLabelValues(string(entryType), string(status)).Inc()
|
||||
journalEntryLatency.WithLabelValues(string(entryType)).Observe(durationSeconds)
|
||||
}
|
||||
|
||||
func recordJournalEntryError(entryType, errorType string) {
|
||||
func recordJournalEntryError(entryType journalEntryType, errorType journalEntryErrorType) {
|
||||
initMetrics()
|
||||
journalEntryErrors.WithLabelValues(entryType, errorType).Inc()
|
||||
journalEntriesTotal.WithLabelValues(entryType, "error").Inc()
|
||||
journalEntryErrors.WithLabelValues(string(entryType), string(errorType)).Inc()
|
||||
journalEntriesTotal.WithLabelValues(string(entryType), string(journalEntryStatusError)).Inc()
|
||||
}
|
||||
|
||||
func recordBalanceQuery(status string, durationSeconds float64) {
|
||||
@@ -128,9 +162,9 @@ func recordBalanceQuery(status string, durationSeconds float64) {
|
||||
balanceQueryLatency.WithLabelValues(status).Observe(durationSeconds)
|
||||
}
|
||||
|
||||
func recordTransactionAmount(currency, entryType string, amount float64) {
|
||||
func recordTransactionAmount(currency string, entryType journalEntryType, amount float64) {
|
||||
initMetrics()
|
||||
transactionAmounts.WithLabelValues(currency, entryType).Observe(amount)
|
||||
transactionAmounts.WithLabelValues(currency, string(entryType)).Observe(amount)
|
||||
}
|
||||
|
||||
func recordAccountOperation(operation, status string) {
|
||||
@@ -138,7 +172,7 @@ func recordAccountOperation(operation, status string) {
|
||||
accountOperationsTotal.WithLabelValues(operation, status).Inc()
|
||||
}
|
||||
|
||||
func recordDuplicateRequest(entryType string) {
|
||||
func recordDuplicateRequest(entryType journalEntryType) {
|
||||
initMetrics()
|
||||
duplicateRequestsTotal.WithLabelValues(entryType).Inc()
|
||||
duplicateRequestsTotal.WithLabelValues(string(entryType)).Inc()
|
||||
}
|
||||
|
||||
@@ -65,7 +65,7 @@ func (s *Service) postCreditResponder(_ context.Context, req *ledgerv1.PostCredi
|
||||
|
||||
existingEntry, err := s.storage.JournalEntries().GetByIdempotencyKey(ctx, orgRef, req.IdempotencyKey)
|
||||
if err == nil && existingEntry != nil {
|
||||
recordDuplicateRequest("credit")
|
||||
recordDuplicateRequest(journalEntryTypeCredit)
|
||||
logger.Info("Duplicate credit request (idempotency)",
|
||||
zap.String("existingEntryID", existingEntry.GetID().Hex()))
|
||||
return &ledgerv1.PostResponse{
|
||||
@@ -75,18 +75,18 @@ func (s *Service) postCreditResponder(_ context.Context, req *ledgerv1.PostCredi
|
||||
}, nil
|
||||
}
|
||||
if err != nil && err != storage.ErrJournalEntryNotFound {
|
||||
recordJournalEntryError("credit", "idempotency_check_failed")
|
||||
recordJournalEntryError(journalEntryTypeCredit, journalEntryErrorIdempotencyCheck)
|
||||
logger.Warn("Failed to check idempotency", zap.Error(err))
|
||||
return nil, merrors.Internal("failed to check idempotency")
|
||||
}
|
||||
|
||||
account, accountRef, err := s.resolveAccount(ctx, strings.TrimSpace(req.LedgerAccountRef), roleModel, orgRef, req.Money.Currency, "account")
|
||||
if err != nil {
|
||||
recordJournalEntryError("credit", "account_resolve_failed")
|
||||
recordJournalEntryError(journalEntryTypeCredit, journalEntryErrorAccountResolve)
|
||||
return nil, err
|
||||
}
|
||||
if err := validateAccountForOrg(account, orgRef, req.Money.Currency); err != nil {
|
||||
recordJournalEntryError("credit", "account_invalid")
|
||||
recordJournalEntryError(journalEntryTypeCredit, journalEntryErrorAccountInvalid)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -159,12 +159,12 @@ func (s *Service) postCreditResponder(_ context.Context, req *ledgerv1.PostCredi
|
||||
|
||||
contraAccount, err := s.resolveSettlementAccount(ctx, orgRef, req.Money.Currency, req.ContraLedgerAccountRef, accountsByRef)
|
||||
if err != nil {
|
||||
recordJournalEntryError("credit", "contra_resolve_failed")
|
||||
recordJournalEntryError(journalEntryTypeCredit, journalEntryErrorContraResolve)
|
||||
return nil, err
|
||||
}
|
||||
contraAccountID := contraAccount.GetID()
|
||||
if contraAccountID == nil {
|
||||
recordJournalEntryError("credit", "contra_missing_id")
|
||||
recordJournalEntryError(journalEntryTypeCredit, journalEntryErrorContraMissingID)
|
||||
return nil, merrors.Internal("contra account missing identifier")
|
||||
}
|
||||
|
||||
@@ -183,7 +183,7 @@ func (s *Service) postCreditResponder(_ context.Context, req *ledgerv1.PostCredi
|
||||
}
|
||||
|
||||
if !entryTotal.IsZero() {
|
||||
recordJournalEntryError("credit", "unbalanced_after_contra")
|
||||
recordJournalEntryError(journalEntryTypeCredit, journalEntryErrorUnbalancedAfterContra)
|
||||
return nil, merrors.Internal("failed to balance journal entry")
|
||||
}
|
||||
|
||||
@@ -237,13 +237,13 @@ func (s *Service) postCreditResponder(_ context.Context, req *ledgerv1.PostCredi
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
recordJournalEntryError("credit", "transaction_failed")
|
||||
recordJournalEntryError(journalEntryTypeCredit, journalEntryErrorTransactionFailed)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
amountFloat, _ := creditAmount.Float64()
|
||||
recordTransactionAmount(req.Money.Currency, "credit", amountFloat)
|
||||
recordJournalEntry("credit", "success", 0)
|
||||
recordTransactionAmount(req.Money.Currency, journalEntryTypeCredit, amountFloat)
|
||||
recordJournalEntry(journalEntryTypeCredit, journalEntryStatusSuccess, 0)
|
||||
return result.(*ledgerv1.PostResponse), nil
|
||||
}
|
||||
}
|
||||
|
||||
@@ -63,7 +63,7 @@ func (s *Service) postDebitResponder(_ context.Context, req *ledgerv1.PostDebitR
|
||||
|
||||
existingEntry, err := s.storage.JournalEntries().GetByIdempotencyKey(ctx, orgRef, req.IdempotencyKey)
|
||||
if err == nil && existingEntry != nil {
|
||||
recordDuplicateRequest("debit")
|
||||
recordDuplicateRequest(journalEntryTypeDebit)
|
||||
logger.Info("Duplicate debit request (idempotency)",
|
||||
zap.String("existingEntryID", existingEntry.GetID().Hex()))
|
||||
return &ledgerv1.PostResponse{
|
||||
@@ -79,11 +79,11 @@ func (s *Service) postDebitResponder(_ context.Context, req *ledgerv1.PostDebitR
|
||||
|
||||
account, accountRef, err := s.resolveAccount(ctx, strings.TrimSpace(req.LedgerAccountRef), roleModel, orgRef, req.Money.Currency, "account")
|
||||
if err != nil {
|
||||
recordJournalEntryError("debit", "account_resolve_failed")
|
||||
recordJournalEntryError(journalEntryTypeDebit, journalEntryErrorAccountResolve)
|
||||
return nil, err
|
||||
}
|
||||
if err := validateAccountForOrg(account, orgRef, req.Money.Currency); err != nil {
|
||||
recordJournalEntryError("debit", "account_invalid")
|
||||
recordJournalEntryError(journalEntryTypeDebit, journalEntryErrorAccountInvalid)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -156,12 +156,12 @@ func (s *Service) postDebitResponder(_ context.Context, req *ledgerv1.PostDebitR
|
||||
|
||||
contraAccount, err := s.resolveSettlementAccount(ctx, orgRef, req.Money.Currency, req.ContraLedgerAccountRef, accountsByRef)
|
||||
if err != nil {
|
||||
recordJournalEntryError("debit", "contra_resolve_failed")
|
||||
recordJournalEntryError(journalEntryTypeDebit, journalEntryErrorContraResolve)
|
||||
return nil, err
|
||||
}
|
||||
contraAccountID := contraAccount.GetID()
|
||||
if contraAccountID == nil {
|
||||
recordJournalEntryError("debit", "contra_missing_id")
|
||||
recordJournalEntryError(journalEntryTypeDebit, journalEntryErrorContraMissingID)
|
||||
return nil, merrors.Internal("contra account missing identifier")
|
||||
}
|
||||
|
||||
@@ -180,7 +180,7 @@ func (s *Service) postDebitResponder(_ context.Context, req *ledgerv1.PostDebitR
|
||||
}
|
||||
|
||||
if !entryTotal.IsZero() {
|
||||
recordJournalEntryError("debit", "unbalanced_after_contra")
|
||||
recordJournalEntryError(journalEntryTypeDebit, journalEntryErrorUnbalancedAfterContra)
|
||||
return nil, merrors.Internal("failed to balance journal entry")
|
||||
}
|
||||
|
||||
@@ -234,13 +234,13 @@ func (s *Service) postDebitResponder(_ context.Context, req *ledgerv1.PostDebitR
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
recordJournalEntryError("debit", "transaction_failed")
|
||||
recordJournalEntryError(journalEntryTypeDebit, journalEntryErrorTransactionFailed)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
amountFloat, _ := debitAmount.Float64()
|
||||
recordTransactionAmount(req.Money.Currency, "debit", amountFloat)
|
||||
recordJournalEntry("debit", "success", 0)
|
||||
recordTransactionAmount(req.Money.Currency, journalEntryTypeDebit, amountFloat)
|
||||
recordJournalEntry(journalEntryTypeDebit, journalEntryStatusSuccess, 0)
|
||||
return result.(*ledgerv1.PostResponse), nil
|
||||
}
|
||||
}
|
||||
|
||||
@@ -60,7 +60,7 @@ func (s *Service) postExternalCreditResponder(_ context.Context, req *ledgerv1.P
|
||||
|
||||
existingEntry, err := s.storage.JournalEntries().GetByIdempotencyKey(ctx, orgRef, req.IdempotencyKey)
|
||||
if err == nil && existingEntry != nil {
|
||||
recordDuplicateRequest("credit")
|
||||
recordDuplicateRequest(journalEntryTypeCredit)
|
||||
logger.Info("Duplicate external credit request (idempotency)",
|
||||
zap.String("existingEntryID", existingEntry.GetID().Hex()))
|
||||
return &ledgerv1.PostResponse{
|
||||
@@ -70,34 +70,34 @@ func (s *Service) postExternalCreditResponder(_ context.Context, req *ledgerv1.P
|
||||
}, nil
|
||||
}
|
||||
if err != nil && err != storage.ErrJournalEntryNotFound {
|
||||
recordJournalEntryError("credit", "idempotency_check_failed")
|
||||
recordJournalEntryError(journalEntryTypeCredit, journalEntryErrorIdempotencyCheck)
|
||||
logger.Warn("Failed to check idempotency", zap.Error(err))
|
||||
return nil, merrors.Internal("failed to check idempotency")
|
||||
}
|
||||
|
||||
account, accountRef, err := s.resolveAccount(ctx, strings.TrimSpace(req.LedgerAccountRef), roleModel, orgRef, req.Money.Currency, "account")
|
||||
if err != nil {
|
||||
recordJournalEntryError("credit", "account_resolve_failed")
|
||||
recordJournalEntryError(journalEntryTypeCredit, journalEntryErrorAccountResolve)
|
||||
return nil, err
|
||||
}
|
||||
if err := validateAccountForOrg(account, orgRef, req.Money.Currency); err != nil {
|
||||
recordJournalEntryError("credit", "account_invalid")
|
||||
recordJournalEntryError(journalEntryTypeCredit, journalEntryErrorAccountInvalid)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
systemAccount, err := s.systemAccount(ctx, pmodel.SystemAccountPurposeExternalSource, req.Money.Currency)
|
||||
if err != nil {
|
||||
recordJournalEntryError("credit", "system_account_resolve_failed")
|
||||
recordJournalEntryError(journalEntryTypeCredit, journalEntryErrorSystemAccountResolve)
|
||||
return nil, err
|
||||
}
|
||||
if err := validateSystemAccount(systemAccount, pmodel.SystemAccountPurposeExternalSource, req.Money.Currency); err != nil {
|
||||
recordJournalEntryError("credit", "system_account_invalid")
|
||||
recordJournalEntryError(journalEntryTypeCredit, journalEntryErrorSystemAccountInvalid)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
systemAccountID := systemAccount.GetID()
|
||||
if systemAccountID == nil {
|
||||
recordJournalEntryError("credit", "system_account_missing_id")
|
||||
recordJournalEntryError(journalEntryTypeCredit, journalEntryErrorSystemAccountMissing)
|
||||
return nil, merrors.Internal("system account missing identifier")
|
||||
}
|
||||
|
||||
@@ -186,7 +186,7 @@ func (s *Service) postExternalCreditResponder(_ context.Context, req *ledgerv1.P
|
||||
}
|
||||
|
||||
if !entryTotal.IsZero() {
|
||||
recordJournalEntryError("credit", "unbalanced_after_contra")
|
||||
recordJournalEntryError(journalEntryTypeCredit, journalEntryErrorUnbalancedAfterContra)
|
||||
return nil, merrors.Internal("failed to balance journal entry")
|
||||
}
|
||||
|
||||
@@ -240,13 +240,13 @@ func (s *Service) postExternalCreditResponder(_ context.Context, req *ledgerv1.P
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
recordJournalEntryError("credit", "transaction_failed")
|
||||
recordJournalEntryError(journalEntryTypeCredit, journalEntryErrorTransactionFailed)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
amountFloat, _ := creditAmount.Float64()
|
||||
recordTransactionAmount(req.Money.Currency, "credit", amountFloat)
|
||||
recordJournalEntry("credit", "success", 0)
|
||||
recordTransactionAmount(req.Money.Currency, journalEntryTypeCredit, amountFloat)
|
||||
recordJournalEntry(journalEntryTypeCredit, journalEntryStatusSuccess, 0)
|
||||
return result.(*ledgerv1.PostResponse), nil
|
||||
}
|
||||
}
|
||||
@@ -293,7 +293,7 @@ func (s *Service) postExternalDebitResponder(_ context.Context, req *ledgerv1.Po
|
||||
|
||||
existingEntry, err := s.storage.JournalEntries().GetByIdempotencyKey(ctx, orgRef, req.IdempotencyKey)
|
||||
if err == nil && existingEntry != nil {
|
||||
recordDuplicateRequest("debit")
|
||||
recordDuplicateRequest(journalEntryTypeDebit)
|
||||
logger.Info("Duplicate external debit request (idempotency)",
|
||||
zap.String("existingEntryID", existingEntry.GetID().Hex()))
|
||||
return &ledgerv1.PostResponse{
|
||||
@@ -303,34 +303,34 @@ func (s *Service) postExternalDebitResponder(_ context.Context, req *ledgerv1.Po
|
||||
}, nil
|
||||
}
|
||||
if err != nil && err != storage.ErrJournalEntryNotFound {
|
||||
recordJournalEntryError("debit", "idempotency_check_failed")
|
||||
recordJournalEntryError(journalEntryTypeDebit, journalEntryErrorIdempotencyCheck)
|
||||
logger.Warn("Failed to check idempotency", zap.Error(err))
|
||||
return nil, merrors.Internal("failed to check idempotency")
|
||||
}
|
||||
|
||||
account, accountRef, err := s.resolveAccount(ctx, strings.TrimSpace(req.LedgerAccountRef), roleModel, orgRef, req.Money.Currency, "account")
|
||||
if err != nil {
|
||||
recordJournalEntryError("debit", "account_resolve_failed")
|
||||
recordJournalEntryError(journalEntryTypeDebit, journalEntryErrorAccountResolve)
|
||||
return nil, err
|
||||
}
|
||||
if err := validateAccountForOrg(account, orgRef, req.Money.Currency); err != nil {
|
||||
recordJournalEntryError("debit", "account_invalid")
|
||||
recordJournalEntryError(journalEntryTypeDebit, journalEntryErrorAccountInvalid)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
systemAccount, err := s.systemAccount(ctx, pmodel.SystemAccountPurposeExternalSink, req.Money.Currency)
|
||||
if err != nil {
|
||||
recordJournalEntryError("debit", "system_account_resolve_failed")
|
||||
recordJournalEntryError(journalEntryTypeDebit, journalEntryErrorSystemAccountResolve)
|
||||
return nil, err
|
||||
}
|
||||
if err := validateSystemAccount(systemAccount, pmodel.SystemAccountPurposeExternalSink, req.Money.Currency); err != nil {
|
||||
recordJournalEntryError("debit", "system_account_invalid")
|
||||
recordJournalEntryError(journalEntryTypeDebit, journalEntryErrorSystemAccountInvalid)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
systemAccountID := systemAccount.GetID()
|
||||
if systemAccountID == nil {
|
||||
recordJournalEntryError("debit", "system_account_missing_id")
|
||||
recordJournalEntryError(journalEntryTypeDebit, journalEntryErrorSystemAccountMissing)
|
||||
return nil, merrors.Internal("system account missing identifier")
|
||||
}
|
||||
|
||||
@@ -419,7 +419,7 @@ func (s *Service) postExternalDebitResponder(_ context.Context, req *ledgerv1.Po
|
||||
}
|
||||
|
||||
if !entryTotal.IsZero() {
|
||||
recordJournalEntryError("debit", "unbalanced_after_contra")
|
||||
recordJournalEntryError(journalEntryTypeDebit, journalEntryErrorUnbalancedAfterContra)
|
||||
return nil, merrors.Internal("failed to balance journal entry")
|
||||
}
|
||||
|
||||
@@ -473,13 +473,13 @@ func (s *Service) postExternalDebitResponder(_ context.Context, req *ledgerv1.Po
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
recordJournalEntryError("debit", "transaction_failed")
|
||||
recordJournalEntryError(journalEntryTypeDebit, journalEntryErrorTransactionFailed)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
amountFloat, _ := debitAmount.Float64()
|
||||
recordTransactionAmount(req.Money.Currency, "debit", amountFloat)
|
||||
recordJournalEntry("debit", "success", 0)
|
||||
recordTransactionAmount(req.Money.Currency, journalEntryTypeDebit, amountFloat)
|
||||
recordJournalEntry(journalEntryTypeDebit, journalEntryStatusSuccess, 0)
|
||||
return result.(*ledgerv1.PostResponse), nil
|
||||
}
|
||||
}
|
||||
|
||||
@@ -76,7 +76,7 @@ func (s *Service) fxResponder(_ context.Context, req *ledgerv1.FXRequest) gsresp
|
||||
// Check for duplicate idempotency key
|
||||
existingEntry, err := s.storage.JournalEntries().GetByIdempotencyKey(ctx, orgRef, req.IdempotencyKey)
|
||||
if err == nil && existingEntry != nil {
|
||||
recordDuplicateRequest("fx")
|
||||
recordDuplicateRequest(journalEntryTypeFX)
|
||||
logger.Info("Duplicate FX request (idempotency)",
|
||||
zap.String("existingEntryID", existingEntry.GetID().Hex()))
|
||||
return &ledgerv1.PostResponse{
|
||||
@@ -244,15 +244,15 @@ func (s *Service) fxResponder(_ context.Context, req *ledgerv1.FXRequest) gsresp
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
recordJournalEntryError("fx", "transaction_failed")
|
||||
recordJournalEntryError(journalEntryTypeFX, journalEntryErrorTransactionFailed)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
fromAmountFloat, _ := fromAmount.Float64()
|
||||
toAmountFloat, _ := toAmount.Float64()
|
||||
recordTransactionAmount(req.FromMoney.Currency, "fx", fromAmountFloat)
|
||||
recordTransactionAmount(req.ToMoney.Currency, "fx", toAmountFloat)
|
||||
recordJournalEntry("fx", "success", 0)
|
||||
recordTransactionAmount(req.FromMoney.Currency, journalEntryTypeFX, fromAmountFloat)
|
||||
recordTransactionAmount(req.ToMoney.Currency, journalEntryTypeFX, toAmountFloat)
|
||||
recordJournalEntry(journalEntryTypeFX, journalEntryStatusSuccess, 0)
|
||||
return result.(*ledgerv1.PostResponse), nil
|
||||
}
|
||||
}
|
||||
|
||||
@@ -86,7 +86,7 @@ func (s *Service) transferResponder(_ context.Context, req *ledgerv1.TransferReq
|
||||
// Check for duplicate idempotency key
|
||||
existingEntry, err := s.storage.JournalEntries().GetByIdempotencyKey(ctx, orgRef, req.IdempotencyKey)
|
||||
if err == nil && existingEntry != nil {
|
||||
recordDuplicateRequest("transfer")
|
||||
recordDuplicateRequest(journalEntryTypeTransfer)
|
||||
logger.Info("Duplicate transfer request (idempotency)",
|
||||
zap.String("existingEntryID", existingEntry.GetID().Hex()))
|
||||
return &ledgerv1.PostResponse{
|
||||
@@ -246,13 +246,13 @@ func (s *Service) transferResponder(_ context.Context, req *ledgerv1.TransferReq
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
recordJournalEntryError("transfer", "failed")
|
||||
recordJournalEntryError(journalEntryTypeTransfer, journalEntryErrorFailed)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
amountFloat, _ := transferAmount.Float64()
|
||||
recordTransactionAmount(req.Money.Currency, "transfer", amountFloat)
|
||||
recordJournalEntry("transfer", "success", 0)
|
||||
recordTransactionAmount(req.Money.Currency, journalEntryTypeTransfer, amountFloat)
|
||||
recordJournalEntry(journalEntryTypeTransfer, journalEntryStatusSuccess, 0)
|
||||
return result.(*ledgerv1.PostResponse), nil
|
||||
}
|
||||
}
|
||||
|
||||
@@ -77,7 +77,7 @@ func NewService(logger mlogger.Logger, repo storage.Repository, prod pmessaging.
|
||||
initMetrics()
|
||||
|
||||
service := &Service{
|
||||
logger: logger.Named("ledger"),
|
||||
logger: logger.Named("service"),
|
||||
storage: repo,
|
||||
producer: prod,
|
||||
msgCfg: msgCfg,
|
||||
@@ -117,17 +117,10 @@ func (s *Service) CreateAccount(ctx context.Context, req *ledgerv1.CreateAccount
|
||||
func (s *Service) PostCreditWithCharges(ctx context.Context, req *ledgerv1.PostCreditRequest) (*ledgerv1.PostResponse, error) {
|
||||
start := time.Now()
|
||||
defer func() {
|
||||
recordJournalEntry("credit", "attempted", time.Since(start).Seconds())
|
||||
recordJournalEntry(journalEntryTypeCredit, journalEntryStatusAttempted, time.Since(start).Seconds())
|
||||
}()
|
||||
|
||||
responder := s.postCreditResponder(ctx, req)
|
||||
resp, err := responder(ctx)
|
||||
|
||||
if err != nil {
|
||||
recordJournalEntryError("credit", "not_implemented")
|
||||
}
|
||||
|
||||
logger := s.logger.With(zap.String("operation", "credit"))
|
||||
logger := s.logger.With(zap.String("operation", discovery.OperationLedgerCredit))
|
||||
if req != nil {
|
||||
logger = logger.With(
|
||||
zap.String("idempotency_key", strings.TrimSpace(req.GetIdempotencyKey())),
|
||||
@@ -147,7 +140,16 @@ func (s *Service) PostCreditWithCharges(ctx context.Context, req *ledgerv1.PostC
|
||||
logger = logger.With(zap.String("contra_ledger_account_ref", contra))
|
||||
}
|
||||
}
|
||||
s.logLedgerOperation("credit", logger, resp, err)
|
||||
s.logLedgerOperationStart(discovery.OperationLedgerCredit, logger)
|
||||
|
||||
responder := s.postCreditResponder(ctx, req)
|
||||
resp, err := responder(ctx)
|
||||
|
||||
if err != nil {
|
||||
recordJournalEntryError(journalEntryTypeCredit, journalEntryErrorNotImplemented)
|
||||
}
|
||||
|
||||
s.logLedgerOperation(discovery.OperationLedgerCredit, logger, resp, err, time.Since(start))
|
||||
|
||||
return resp, err
|
||||
}
|
||||
@@ -156,17 +158,10 @@ func (s *Service) PostCreditWithCharges(ctx context.Context, req *ledgerv1.PostC
|
||||
func (s *Service) PostExternalCreditWithCharges(ctx context.Context, req *ledgerv1.PostCreditRequest) (*ledgerv1.PostResponse, error) {
|
||||
start := time.Now()
|
||||
defer func() {
|
||||
recordJournalEntry("credit", "attempted", time.Since(start).Seconds())
|
||||
recordJournalEntry(journalEntryTypeCredit, journalEntryStatusAttempted, time.Since(start).Seconds())
|
||||
}()
|
||||
|
||||
responder := s.postExternalCreditResponder(ctx, req)
|
||||
resp, err := responder(ctx)
|
||||
|
||||
if err != nil {
|
||||
recordJournalEntryError("credit", "failed")
|
||||
}
|
||||
|
||||
logger := s.logger.With(zap.String("operation", "external_credit"))
|
||||
logger := s.logger.With(zap.String("operation", discovery.OperationExternalCredit))
|
||||
if req != nil {
|
||||
logger = logger.With(
|
||||
zap.String("idempotency_key", strings.TrimSpace(req.GetIdempotencyKey())),
|
||||
@@ -183,7 +178,16 @@ func (s *Service) PostExternalCreditWithCharges(ctx context.Context, req *ledger
|
||||
logger = logger.With(zap.String("role", role.String()))
|
||||
}
|
||||
}
|
||||
s.logLedgerOperation("external_credit", logger, resp, err)
|
||||
s.logLedgerOperationStart(discovery.OperationExternalCredit, logger)
|
||||
|
||||
responder := s.postExternalCreditResponder(ctx, req)
|
||||
resp, err := responder(ctx)
|
||||
|
||||
if err != nil {
|
||||
recordJournalEntryError(journalEntryTypeCredit, journalEntryErrorFailed)
|
||||
}
|
||||
|
||||
s.logLedgerOperation(discovery.OperationExternalCredit, logger, resp, err, time.Since(start))
|
||||
|
||||
return resp, err
|
||||
}
|
||||
@@ -192,17 +196,10 @@ func (s *Service) PostExternalCreditWithCharges(ctx context.Context, req *ledger
|
||||
func (s *Service) PostDebitWithCharges(ctx context.Context, req *ledgerv1.PostDebitRequest) (*ledgerv1.PostResponse, error) {
|
||||
start := time.Now()
|
||||
defer func() {
|
||||
recordJournalEntry("debit", "attempted", time.Since(start).Seconds())
|
||||
recordJournalEntry(journalEntryTypeDebit, journalEntryStatusAttempted, time.Since(start).Seconds())
|
||||
}()
|
||||
|
||||
responder := s.postDebitResponder(ctx, req)
|
||||
resp, err := responder(ctx)
|
||||
|
||||
if err != nil {
|
||||
recordJournalEntryError("debit", "failed")
|
||||
}
|
||||
|
||||
logger := s.logger.With(zap.String("operation", "debit"))
|
||||
logger := s.logger.With(zap.String("operation", discovery.OperationLedgerDebit))
|
||||
if req != nil {
|
||||
logger = logger.With(
|
||||
zap.String("idempotency_key", strings.TrimSpace(req.GetIdempotencyKey())),
|
||||
@@ -222,7 +219,16 @@ func (s *Service) PostDebitWithCharges(ctx context.Context, req *ledgerv1.PostDe
|
||||
logger = logger.With(zap.String("contra_ledger_account_ref", contra))
|
||||
}
|
||||
}
|
||||
s.logLedgerOperation("debit", logger, resp, err)
|
||||
s.logLedgerOperationStart(discovery.OperationLedgerDebit, logger)
|
||||
|
||||
responder := s.postDebitResponder(ctx, req)
|
||||
resp, err := responder(ctx)
|
||||
|
||||
if err != nil {
|
||||
recordJournalEntryError(journalEntryTypeDebit, journalEntryErrorFailed)
|
||||
}
|
||||
|
||||
s.logLedgerOperation(discovery.OperationLedgerDebit, logger, resp, err, time.Since(start))
|
||||
|
||||
return resp, err
|
||||
}
|
||||
@@ -231,17 +237,10 @@ func (s *Service) PostDebitWithCharges(ctx context.Context, req *ledgerv1.PostDe
|
||||
func (s *Service) PostExternalDebitWithCharges(ctx context.Context, req *ledgerv1.PostDebitRequest) (*ledgerv1.PostResponse, error) {
|
||||
start := time.Now()
|
||||
defer func() {
|
||||
recordJournalEntry("debit", "attempted", time.Since(start).Seconds())
|
||||
recordJournalEntry(journalEntryTypeDebit, journalEntryStatusAttempted, time.Since(start).Seconds())
|
||||
}()
|
||||
|
||||
responder := s.postExternalDebitResponder(ctx, req)
|
||||
resp, err := responder(ctx)
|
||||
|
||||
if err != nil {
|
||||
recordJournalEntryError("debit", "failed")
|
||||
}
|
||||
|
||||
logger := s.logger.With(zap.String("operation", "external_debit"))
|
||||
logger := s.logger.With(zap.String("operation", discovery.OperationExternalDebit))
|
||||
if req != nil {
|
||||
logger = logger.With(
|
||||
zap.String("idempotency_key", strings.TrimSpace(req.GetIdempotencyKey())),
|
||||
@@ -258,7 +257,16 @@ func (s *Service) PostExternalDebitWithCharges(ctx context.Context, req *ledgerv
|
||||
logger = logger.With(zap.String("role", role.String()))
|
||||
}
|
||||
}
|
||||
s.logLedgerOperation("external_debit", logger, resp, err)
|
||||
s.logLedgerOperationStart(discovery.OperationExternalDebit, logger)
|
||||
|
||||
responder := s.postExternalDebitResponder(ctx, req)
|
||||
resp, err := responder(ctx)
|
||||
|
||||
if err != nil {
|
||||
recordJournalEntryError(journalEntryTypeDebit, journalEntryErrorFailed)
|
||||
}
|
||||
|
||||
s.logLedgerOperation(discovery.OperationExternalDebit, logger, resp, err, time.Since(start))
|
||||
|
||||
return resp, err
|
||||
}
|
||||
@@ -267,17 +275,10 @@ func (s *Service) PostExternalDebitWithCharges(ctx context.Context, req *ledgerv
|
||||
func (s *Service) TransferInternal(ctx context.Context, req *ledgerv1.TransferRequest) (*ledgerv1.PostResponse, error) {
|
||||
start := time.Now()
|
||||
defer func() {
|
||||
recordJournalEntry("transfer", "attempted", time.Since(start).Seconds())
|
||||
recordJournalEntry(journalEntryTypeTransfer, journalEntryStatusAttempted, time.Since(start).Seconds())
|
||||
}()
|
||||
|
||||
responder := s.transferResponder(ctx, req)
|
||||
resp, err := responder(ctx)
|
||||
|
||||
if err != nil {
|
||||
recordJournalEntryError("transfer", "failed")
|
||||
}
|
||||
|
||||
logger := s.logger.With(zap.String("operation", "transfer"))
|
||||
logger := s.logger.With(zap.String("operation", discovery.OperationLedgerTransfer))
|
||||
if req != nil {
|
||||
logger = logger.With(
|
||||
zap.String("idempotency_key", strings.TrimSpace(req.GetIdempotencyKey())),
|
||||
@@ -298,7 +299,16 @@ func (s *Service) TransferInternal(ctx context.Context, req *ledgerv1.TransferRe
|
||||
logger = logger.With(zap.String("to_role", role.String()))
|
||||
}
|
||||
}
|
||||
s.logLedgerOperation("transfer", logger, resp, err)
|
||||
s.logLedgerOperationStart(discovery.OperationLedgerTransfer, logger)
|
||||
|
||||
responder := s.transferResponder(ctx, req)
|
||||
resp, err := responder(ctx)
|
||||
|
||||
if err != nil {
|
||||
recordJournalEntryError(journalEntryTypeTransfer, journalEntryErrorFailed)
|
||||
}
|
||||
|
||||
s.logLedgerOperation(discovery.OperationLedgerTransfer, logger, resp, err, time.Since(start))
|
||||
|
||||
return resp, err
|
||||
}
|
||||
@@ -307,17 +317,10 @@ func (s *Service) TransferInternal(ctx context.Context, req *ledgerv1.TransferRe
|
||||
func (s *Service) ApplyFXWithCharges(ctx context.Context, req *ledgerv1.FXRequest) (*ledgerv1.PostResponse, error) {
|
||||
start := time.Now()
|
||||
defer func() {
|
||||
recordJournalEntry("fx", "attempted", time.Since(start).Seconds())
|
||||
recordJournalEntry(journalEntryTypeFX, journalEntryStatusAttempted, time.Since(start).Seconds())
|
||||
}()
|
||||
|
||||
responder := s.fxResponder(ctx, req)
|
||||
resp, err := responder(ctx)
|
||||
|
||||
if err != nil {
|
||||
recordJournalEntryError("fx", "failed")
|
||||
}
|
||||
|
||||
logger := s.logger.With(zap.String("operation", "fx"))
|
||||
logger := s.logger.With(zap.String("operation", discovery.OperationLedgerFX))
|
||||
if req != nil {
|
||||
logger = logger.With(
|
||||
zap.String("idempotency_key", strings.TrimSpace(req.GetIdempotencyKey())),
|
||||
@@ -341,7 +344,16 @@ func (s *Service) ApplyFXWithCharges(ctx context.Context, req *ledgerv1.FXReques
|
||||
logger = logger.With(zap.String("rate", rate))
|
||||
}
|
||||
}
|
||||
s.logLedgerOperation("fx", logger, resp, err)
|
||||
s.logLedgerOperationStart(discovery.OperationLedgerFX, logger)
|
||||
|
||||
responder := s.fxResponder(ctx, req)
|
||||
resp, err := responder(ctx)
|
||||
|
||||
if err != nil {
|
||||
recordJournalEntryError(journalEntryTypeFX, journalEntryErrorFailed)
|
||||
}
|
||||
|
||||
s.logLedgerOperation(discovery.OperationLedgerFX, logger, resp, err, time.Since(start))
|
||||
|
||||
return resp, err
|
||||
}
|
||||
@@ -365,23 +377,42 @@ func (s *Service) GetJournalEntry(ctx context.Context, req *ledgerv1.GetEntryReq
|
||||
return responder(ctx)
|
||||
}
|
||||
|
||||
func (s *Service) logLedgerOperation(op string, logger mlogger.Logger, resp *ledgerv1.PostResponse, err error) {
|
||||
func (s *Service) logLedgerOperationStart(op string, logger mlogger.Logger) {
|
||||
if logger == nil {
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
logger.Warn(fmt.Sprintf("ledger %s failed", op), zap.Error(err))
|
||||
logger.Debug("Ledger operation execution started", zap.String("operation_name", op))
|
||||
}
|
||||
|
||||
func (s *Service) logLedgerOperation(op string, logger mlogger.Logger, resp *ledgerv1.PostResponse, err error, duration time.Duration) {
|
||||
if logger == nil {
|
||||
return
|
||||
}
|
||||
entryRef := ""
|
||||
if resp != nil {
|
||||
entryRef = strings.TrimSpace(resp.GetJournalEntryRef())
|
||||
}
|
||||
if entryRef == "" {
|
||||
logger.Info(fmt.Sprintf("ledger %s posted", op))
|
||||
status := "succeeded"
|
||||
fields := []zap.Field{
|
||||
zap.String("operation_name", op),
|
||||
zap.String("status", status),
|
||||
zap.Int64("duration_ms", duration.Milliseconds()),
|
||||
}
|
||||
if entryRef != "" {
|
||||
fields = append(fields, zap.String("journal_entry_ref", entryRef))
|
||||
}
|
||||
if err != nil {
|
||||
fields[1] = zap.String("status", "failed")
|
||||
logger.Debug("Ledger operation execution completed", append(fields, zap.Error(err))...)
|
||||
logger.Warn("Ledger operation failed", zap.String("operation_name", op), zap.Error(err))
|
||||
return
|
||||
}
|
||||
logger.Info(fmt.Sprintf("ledger %s posted", op), zap.String("journal_entry_ref", entryRef))
|
||||
logger.Debug("Ledger operation execution completed", fields...)
|
||||
if entryRef == "" {
|
||||
logger.Info("Ledger operation posted", zap.String("operation_name", op))
|
||||
return
|
||||
}
|
||||
logger.Info("Ledger operation posted", zap.String("operation_name", op), zap.String("journal_entry_ref", entryRef))
|
||||
}
|
||||
|
||||
func (s *Service) Shutdown() {
|
||||
@@ -402,7 +433,7 @@ func (s *Service) startDiscoveryAnnouncer() {
|
||||
}
|
||||
announce := discovery.Announcement{
|
||||
Service: "LEDGER",
|
||||
Operations: []string{"balance.read", "ledger.debit", "ledger.credit", "external.credit", "external.debit"},
|
||||
Operations: discovery.LedgerServiceOperations(),
|
||||
InvokeURI: s.invokeURI,
|
||||
Version: appversion.Create().Short(),
|
||||
}
|
||||
@@ -428,8 +459,7 @@ func (s *Service) startOutboxReliableProducer() error {
|
||||
}
|
||||
s.outbox.producer = reliableProducer
|
||||
if s.outbox.producer == nil || s.producer == nil {
|
||||
s.logger.Info("Outbox reliable publisher disabled",
|
||||
zap.Bool("enabled", settings.Enabled))
|
||||
s.logger.Info("Outbox reliable publisher disabled", zap.Bool("enabled", settings.Enabled))
|
||||
return
|
||||
}
|
||||
s.logger.Info("Outbox reliable publisher configured",
|
||||
|
||||
@@ -120,7 +120,7 @@ func CreateAPI(a api.API) (*NotificationAPI, error) {
|
||||
|
||||
announce := discovery.Announcement{
|
||||
Service: "NOTIFICATIONS",
|
||||
Operations: []string{"notify.send"},
|
||||
Operations: []string{discovery.OperationNotifySend},
|
||||
Version: appversion.Create().Short(),
|
||||
}
|
||||
p.announcer = discovery.NewAnnouncer(p.logger, a.Register().Producer(), string(mservice.Notifications), announce)
|
||||
|
||||
@@ -53,8 +53,8 @@ func (i *Imp) startDiscoveryAnnouncer(cfg *config, producer msg.Producer) {
|
||||
announce := discovery.Announcement{
|
||||
Service: "PAYMENTS_METHODS",
|
||||
Operations: []string{
|
||||
"payment_methods.manage",
|
||||
"payment_methods.read",
|
||||
discovery.OperationPaymentMethodsManage,
|
||||
discovery.OperationPaymentMethodsRead,
|
||||
},
|
||||
InvokeURI: invokeURI,
|
||||
Version: appversion.Create().Short(),
|
||||
|
||||
@@ -1,18 +1,14 @@
|
||||
package serverimp
|
||||
|
||||
import (
|
||||
oracleclient "github.com/tech/sendico/fx/oracle/client"
|
||||
mntxclient "github.com/tech/sendico/gateway/mntx/client"
|
||||
ledgerclient "github.com/tech/sendico/ledger/client"
|
||||
"github.com/tech/sendico/payments/orchestrator/internal/service/orchestrator"
|
||||
feesv1 "github.com/tech/sendico/pkg/proto/billing/fees/v1"
|
||||
)
|
||||
|
||||
type orchestratorDeps struct {
|
||||
feesClient feesv1.FeeEngineClient
|
||||
ledgerClient ledgerclient.Client
|
||||
mntxClient mntxclient.Client
|
||||
oracleClient oracleclient.Client
|
||||
gatewayInvokeResolver orchestrator.GatewayInvokeResolver
|
||||
}
|
||||
|
||||
@@ -26,9 +22,7 @@ func (i *Imp) initDependencies(_ *config) *orchestratorDeps {
|
||||
}
|
||||
|
||||
i.discoveryClients = newDiscoveryClientResolver(i.logger, i.discoveryReg)
|
||||
deps.feesClient = &discoveryFeeClient{resolver: i.discoveryClients}
|
||||
deps.ledgerClient = &discoveryLedgerClient{resolver: i.discoveryClients}
|
||||
deps.oracleClient = &discoveryOracleClient{resolver: i.discoveryClients}
|
||||
deps.mntxClient = &discoveryMntxClient{resolver: i.discoveryClients}
|
||||
deps.gatewayInvokeResolver = discoveryGatewayInvokeResolver{resolver: i.discoveryClients}
|
||||
return deps
|
||||
@@ -39,9 +33,6 @@ func (i *Imp) buildServiceOptions(cfg *config, deps *orchestratorDeps) []orchest
|
||||
return nil
|
||||
}
|
||||
opts := []orchestrator.Option{}
|
||||
if deps.feesClient != nil {
|
||||
opts = append(opts, orchestrator.WithFeeEngine(deps.feesClient, cfg.Fees.callTimeout()))
|
||||
}
|
||||
if deps.ledgerClient != nil {
|
||||
opts = append(opts, orchestrator.WithLedgerClient(deps.ledgerClient))
|
||||
}
|
||||
@@ -49,16 +40,12 @@ func (i *Imp) buildServiceOptions(cfg *config, deps *orchestratorDeps) []orchest
|
||||
opts = append(opts, orchestrator.WithMntxGateway(deps.mntxClient))
|
||||
}
|
||||
|
||||
opts = append(opts, orchestrator.WithMaxFXQuoteTTLMillis(cfg.maxFXQuoteTTLMillis()))
|
||||
if deps.gatewayInvokeResolver != nil {
|
||||
opts = append(opts, orchestrator.WithGatewayInvokeResolver(deps.gatewayInvokeResolver))
|
||||
}
|
||||
if routes := buildCardGatewayRoutes(cfg.CardGateways); len(routes) > 0 {
|
||||
opts = append(opts, orchestrator.WithCardGatewayRoutes(routes))
|
||||
}
|
||||
if feeAccounts := buildFeeLedgerAccounts(cfg.FeeAccounts); len(feeAccounts) > 0 {
|
||||
opts = append(opts, orchestrator.WithFeeLedgerAccounts(feeAccounts))
|
||||
}
|
||||
if registry := buildGatewayRegistry(i.logger, cfg.GatewayInstances, i.discoveryReg); registry != nil {
|
||||
opts = append(opts, orchestrator.WithGatewayRegistry(registry))
|
||||
}
|
||||
|
||||
@@ -33,7 +33,7 @@ func (i *Imp) initDiscovery(cfg *config) {
|
||||
}
|
||||
announce := discovery.Announcement{
|
||||
Service: "PAYMENTS_ORCHESTRATOR",
|
||||
Operations: []string{"payment.initiate"},
|
||||
Operations: []string{discovery.OperationPaymentInitiate},
|
||||
InvokeURI: cfg.GRPC.DiscoveryInvokeURI(),
|
||||
Version: appversion.Create().Short(),
|
||||
}
|
||||
|
||||
@@ -32,6 +32,11 @@ var (
|
||||
ledgerServiceNames = []string{"LEDGER", string(mservice.Ledger)}
|
||||
oracleServiceNames = []string{"FX_ORACLE", string(mservice.FXOracle)}
|
||||
mntxServiceNames = []string{"CARD_RAIL_GATEWAY", string(mservice.MntxGateway)}
|
||||
|
||||
feesRequiredOps = []string{discovery.OperationFeeCalc}
|
||||
ledgerRequiredOps = discovery.LedgerServiceOperations()
|
||||
oracleRequiredOps = []string{discovery.OperationFXQuote}
|
||||
mntxRequiredOps = discovery.CardPayoutRailGatewayOperations()
|
||||
)
|
||||
|
||||
type discoveryEndpoint struct {
|
||||
@@ -109,27 +114,27 @@ func (r *discoveryClientResolver) Close() {
|
||||
}
|
||||
|
||||
func (r *discoveryClientResolver) FeesAvailable() bool {
|
||||
_, ok := r.findEntry("fees", feesServiceNames, "", "")
|
||||
_, ok := r.findEntry("fees", feesServiceNames, "", "", feesRequiredOps)
|
||||
return ok
|
||||
}
|
||||
|
||||
func (r *discoveryClientResolver) LedgerAvailable() bool {
|
||||
_, ok := r.findEntry("ledger", ledgerServiceNames, "", "")
|
||||
_, ok := r.findEntry("ledger", ledgerServiceNames, "", "", ledgerRequiredOps)
|
||||
return ok
|
||||
}
|
||||
|
||||
func (r *discoveryClientResolver) OracleAvailable() bool {
|
||||
_, ok := r.findEntry("oracle", oracleServiceNames, "", "")
|
||||
_, ok := r.findEntry("oracle", oracleServiceNames, "", "", oracleRequiredOps)
|
||||
return ok
|
||||
}
|
||||
|
||||
func (r *discoveryClientResolver) MntxAvailable() bool {
|
||||
_, ok := r.findEntry("mntx", mntxServiceNames, "", "")
|
||||
_, ok := r.findEntry("mntx", mntxServiceNames, "", "", mntxRequiredOps)
|
||||
return ok
|
||||
}
|
||||
|
||||
func (r *discoveryClientResolver) FeesClient(ctx context.Context) (feesv1.FeeEngineClient, error) {
|
||||
entry, ok := r.findEntry("fees", feesServiceNames, "", "")
|
||||
entry, ok := r.findEntry("fees", feesServiceNames, "", "", feesRequiredOps)
|
||||
if !ok {
|
||||
return nil, merrors.NoData("discovery: fees service unavailable")
|
||||
}
|
||||
@@ -160,7 +165,7 @@ func (r *discoveryClientResolver) FeesClient(ctx context.Context) (feesv1.FeeEng
|
||||
}
|
||||
|
||||
func (r *discoveryClientResolver) LedgerClient(ctx context.Context) (ledgerclient.Client, error) {
|
||||
entry, ok := r.findEntry("ledger", ledgerServiceNames, "", "")
|
||||
entry, ok := r.findEntry("ledger", ledgerServiceNames, "", "", ledgerRequiredOps)
|
||||
if !ok {
|
||||
return nil, merrors.NoData("discovery: ledger service unavailable")
|
||||
}
|
||||
@@ -194,7 +199,7 @@ func (r *discoveryClientResolver) LedgerClient(ctx context.Context) (ledgerclien
|
||||
}
|
||||
|
||||
func (r *discoveryClientResolver) OracleClient(ctx context.Context) (oracleclient.Client, error) {
|
||||
entry, ok := r.findEntry("oracle", oracleServiceNames, "", "")
|
||||
entry, ok := r.findEntry("oracle", oracleServiceNames, "", "", oracleRequiredOps)
|
||||
if !ok {
|
||||
return nil, merrors.NoData("discovery: oracle service unavailable")
|
||||
}
|
||||
@@ -228,7 +233,7 @@ func (r *discoveryClientResolver) OracleClient(ctx context.Context) (oracleclien
|
||||
}
|
||||
|
||||
func (r *discoveryClientResolver) MntxClient(ctx context.Context) (mntxclient.Client, error) {
|
||||
entry, ok := r.findEntry("mntx", mntxServiceNames, "", "")
|
||||
entry, ok := r.findEntry("mntx", mntxServiceNames, "", "", mntxRequiredOps)
|
||||
if !ok {
|
||||
return nil, merrors.NoData("discovery: mntx service unavailable")
|
||||
}
|
||||
@@ -316,14 +321,19 @@ func (r *discoveryClientResolver) PaymentGatewayClient(ctx context.Context, invo
|
||||
return client, nil
|
||||
}
|
||||
|
||||
func (r *discoveryClientResolver) findEntry(key string, services []string, rail string, network string) (*discovery.RegistryEntry, bool) {
|
||||
func (r *discoveryClientResolver) findEntry(key string, services []string, rail string, network string, requiredOps []string) (*discovery.RegistryEntry, bool) {
|
||||
if r == nil || r.registry == nil {
|
||||
r.logMissing(key, "discovery registry unavailable", "", nil)
|
||||
return nil, false
|
||||
}
|
||||
|
||||
type discoveryMatch struct {
|
||||
entry discovery.RegistryEntry
|
||||
opMatch bool
|
||||
}
|
||||
|
||||
entries := r.registry.List(time.Now(), true)
|
||||
matches := make([]discovery.RegistryEntry, 0)
|
||||
matches := make([]discoveryMatch, 0)
|
||||
for _, entry := range entries {
|
||||
if !matchesService(entry.Service, services) {
|
||||
continue
|
||||
@@ -334,7 +344,10 @@ func (r *discoveryClientResolver) findEntry(key string, services []string, rail
|
||||
if network != "" && !strings.EqualFold(strings.TrimSpace(entry.Network), network) {
|
||||
continue
|
||||
}
|
||||
matches = append(matches, entry)
|
||||
matches = append(matches, discoveryMatch{
|
||||
entry: entry,
|
||||
opMatch: discovery.HasAnyOperation(entry.Operations, requiredOps),
|
||||
})
|
||||
}
|
||||
|
||||
if len(matches) == 0 {
|
||||
@@ -343,16 +356,19 @@ func (r *discoveryClientResolver) findEntry(key string, services []string, rail
|
||||
}
|
||||
|
||||
sort.Slice(matches, func(i, j int) bool {
|
||||
if matches[i].RoutingPriority != matches[j].RoutingPriority {
|
||||
return matches[i].RoutingPriority > matches[j].RoutingPriority
|
||||
if matches[i].opMatch != matches[j].opMatch {
|
||||
return matches[i].opMatch
|
||||
}
|
||||
if matches[i].ID != matches[j].ID {
|
||||
return matches[i].ID < matches[j].ID
|
||||
if matches[i].entry.RoutingPriority != matches[j].entry.RoutingPriority {
|
||||
return matches[i].entry.RoutingPriority > matches[j].entry.RoutingPriority
|
||||
}
|
||||
return matches[i].InstanceID < matches[j].InstanceID
|
||||
if matches[i].entry.ID != matches[j].entry.ID {
|
||||
return matches[i].entry.ID < matches[j].entry.ID
|
||||
}
|
||||
return matches[i].entry.InstanceID < matches[j].entry.InstanceID
|
||||
})
|
||||
|
||||
entry := matches[0]
|
||||
entry := matches[0].entry
|
||||
entryKey := discoveryEntryKey(entry)
|
||||
r.logSelection(key, entryKey, entry)
|
||||
return &entry, true
|
||||
|
||||
@@ -134,6 +134,22 @@ func (c *discoveryLedgerClient) PostDebitWithCharges(ctx context.Context, req *l
|
||||
return client.PostDebitWithCharges(ctx, req)
|
||||
}
|
||||
|
||||
func (c *discoveryLedgerClient) PostExternalCreditWithCharges(ctx context.Context, req *ledgerv1.PostCreditRequest) (*ledgerv1.PostResponse, error) {
|
||||
client, err := c.resolver.LedgerClient(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return client.PostExternalCreditWithCharges(ctx, req)
|
||||
}
|
||||
|
||||
func (c *discoveryLedgerClient) PostExternalDebitWithCharges(ctx context.Context, req *ledgerv1.PostDebitRequest) (*ledgerv1.PostResponse, error) {
|
||||
client, err := c.resolver.LedgerClient(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return client.PostExternalDebitWithCharges(ctx, req)
|
||||
}
|
||||
|
||||
func (c *discoveryLedgerClient) ApplyFXWithCharges(ctx context.Context, req *ledgerv1.FXRequest) (*ledgerv1.PostResponse, error) {
|
||||
client, err := c.resolver.LedgerClient(ctx)
|
||||
if err != nil {
|
||||
|
||||
@@ -62,17 +62,42 @@ func (e *gatewayLedgerExecutor) ExecuteLedger(ctx context.Context, req sexec.Ste
|
||||
return nil, err
|
||||
}
|
||||
|
||||
transferReq := &ledgerv1.TransferRequest{
|
||||
IdempotencyKey: ledgerStepIdempotencyKey(req.Payment, req.Step),
|
||||
OrganizationRef: req.Payment.OrganizationRef.Hex(),
|
||||
Money: amount,
|
||||
Description: ledgerDescription(req.Step),
|
||||
Metadata: ledgerTransferMetadata(req.Payment, req.Step, roles),
|
||||
FromRole: ledgerRoleToProto(roles.from),
|
||||
ToRole: ledgerRoleToProto(roles.to),
|
||||
}
|
||||
idempotencyKey := ledgerStepIdempotencyKey(req.Payment, req.Step)
|
||||
organizationRef := req.Payment.OrganizationRef.Hex()
|
||||
description := ledgerDescription(req.Step)
|
||||
metadata := ledgerTransferMetadata(req.Payment, req.Step, roles)
|
||||
|
||||
resp, err := e.ledgerClient.TransferInternal(ctx, transferReq)
|
||||
var resp *ledgerv1.PostResponse
|
||||
switch action {
|
||||
case model.RailOperationExternalCredit:
|
||||
resp, err = e.ledgerClient.PostExternalCreditWithCharges(ctx, &ledgerv1.PostCreditRequest{
|
||||
IdempotencyKey: idempotencyKey,
|
||||
OrganizationRef: organizationRef,
|
||||
Money: amount,
|
||||
Description: description,
|
||||
Metadata: metadata,
|
||||
Role: ledgerRoleToProto(roles.to),
|
||||
})
|
||||
case model.RailOperationExternalDebit:
|
||||
resp, err = e.ledgerClient.PostExternalDebitWithCharges(ctx, &ledgerv1.PostDebitRequest{
|
||||
IdempotencyKey: idempotencyKey,
|
||||
OrganizationRef: organizationRef,
|
||||
Money: amount,
|
||||
Description: description,
|
||||
Metadata: metadata,
|
||||
Role: ledgerRoleToProto(roles.from),
|
||||
})
|
||||
default:
|
||||
resp, err = e.ledgerClient.TransferInternal(ctx, &ledgerv1.TransferRequest{
|
||||
IdempotencyKey: idempotencyKey,
|
||||
OrganizationRef: organizationRef,
|
||||
Money: amount,
|
||||
Description: description,
|
||||
Metadata: metadata,
|
||||
FromRole: ledgerRoleToProto(roles.from),
|
||||
ToRole: ledgerRoleToProto(roles.to),
|
||||
})
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -80,6 +80,146 @@ func TestGatewayLedgerExecutor_ExecuteLedger_CreditUsesSourceAmountAndDefaultRol
|
||||
}
|
||||
}
|
||||
|
||||
func TestGatewayLedgerExecutor_ExecuteLedger_ExternalCreditUsesPostCreditWithCharges(t *testing.T) {
|
||||
orgID := bson.NewObjectID()
|
||||
payment := testLedgerExecutorPayment(orgID)
|
||||
|
||||
var (
|
||||
postReq *ledgerv1.PostCreditRequest
|
||||
transferCalled bool
|
||||
)
|
||||
executor := &gatewayLedgerExecutor{
|
||||
ledgerClient: &ledgerclient.Fake{
|
||||
PostExternalCreditWithChargesFn: func(_ context.Context, req *ledgerv1.PostCreditRequest) (*ledgerv1.PostResponse, error) {
|
||||
postReq = req
|
||||
return &ledgerv1.PostResponse{JournalEntryRef: "entry-ext-credit"}, nil
|
||||
},
|
||||
TransferInternalFn: func(_ context.Context, _ *ledgerv1.TransferRequest) (*ledgerv1.PostResponse, error) {
|
||||
transferCalled = true
|
||||
return &ledgerv1.PostResponse{JournalEntryRef: "entry-transfer"}, nil
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
out, err := executor.ExecuteLedger(context.Background(), sexec.StepRequest{
|
||||
Payment: payment,
|
||||
Step: xplan.Step{
|
||||
StepRef: "edge_1_2_ledger_credit",
|
||||
StepCode: "edge.1_2.ledger.credit",
|
||||
Action: model.RailOperationExternalCredit,
|
||||
Rail: model.RailLedger,
|
||||
},
|
||||
StepExecution: agg.StepExecution{
|
||||
StepRef: "edge_1_2_ledger_credit",
|
||||
StepCode: "edge.1_2.ledger.credit",
|
||||
Attempt: 1,
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("ExecuteLedger returned error: %v", err)
|
||||
}
|
||||
if out == nil {
|
||||
t.Fatal("expected output")
|
||||
}
|
||||
if postReq == nil {
|
||||
t.Fatal("expected external credit request")
|
||||
}
|
||||
if transferCalled {
|
||||
t.Fatal("expected external credit to skip transfer")
|
||||
}
|
||||
if got, want := postReq.GetMoney().GetAmount(), "1.000000"; got != want {
|
||||
t.Fatalf("money.amount mismatch: got=%q want=%q", got, want)
|
||||
}
|
||||
if got, want := postReq.GetMoney().GetCurrency(), "USDT"; got != want {
|
||||
t.Fatalf("money.currency mismatch: got=%q want=%q", got, want)
|
||||
}
|
||||
if got, want := postReq.GetRole(), ledgerv1.AccountRole_ACCOUNT_ROLE_OPERATING; got != want {
|
||||
t.Fatalf("role mismatch: got=%v want=%v", got, want)
|
||||
}
|
||||
if got, want := out.StepExecution.State, agg.StepStateCompleted; got != want {
|
||||
t.Fatalf("state mismatch: got=%q want=%q", got, want)
|
||||
}
|
||||
if len(out.StepExecution.ExternalRefs) != 1 {
|
||||
t.Fatalf("expected one external ref, got=%d", len(out.StepExecution.ExternalRefs))
|
||||
}
|
||||
if got, want := out.StepExecution.ExternalRefs[0].Ref, "entry-ext-credit"; got != want {
|
||||
t.Fatalf("external ref value mismatch: got=%q want=%q", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGatewayLedgerExecutor_ExecuteLedger_ExternalDebitUsesPostDebitWithCharges(t *testing.T) {
|
||||
orgID := bson.NewObjectID()
|
||||
payment := testLedgerExecutorPayment(orgID)
|
||||
|
||||
var (
|
||||
postReq *ledgerv1.PostDebitRequest
|
||||
transferCalled bool
|
||||
)
|
||||
executor := &gatewayLedgerExecutor{
|
||||
ledgerClient: &ledgerclient.Fake{
|
||||
PostExternalDebitWithChargesFn: func(_ context.Context, req *ledgerv1.PostDebitRequest) (*ledgerv1.PostResponse, error) {
|
||||
postReq = req
|
||||
return &ledgerv1.PostResponse{JournalEntryRef: "entry-ext-debit"}, nil
|
||||
},
|
||||
TransferInternalFn: func(_ context.Context, _ *ledgerv1.TransferRequest) (*ledgerv1.PostResponse, error) {
|
||||
transferCalled = true
|
||||
return &ledgerv1.PostResponse{JournalEntryRef: "entry-transfer"}, nil
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
out, err := executor.ExecuteLedger(context.Background(), sexec.StepRequest{
|
||||
Payment: payment,
|
||||
Step: xplan.Step{
|
||||
StepRef: "edge_3_4_ledger_debit",
|
||||
StepCode: "edge.3_4.ledger.debit",
|
||||
Action: model.RailOperationExternalDebit,
|
||||
Rail: model.RailLedger,
|
||||
Metadata: map[string]string{
|
||||
"mode": "finalize_debit",
|
||||
},
|
||||
},
|
||||
StepExecution: agg.StepExecution{
|
||||
StepRef: "edge_3_4_ledger_debit",
|
||||
StepCode: "edge.3_4.ledger.debit",
|
||||
Attempt: 1,
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("ExecuteLedger returned error: %v", err)
|
||||
}
|
||||
if out == nil {
|
||||
t.Fatal("expected output")
|
||||
}
|
||||
if postReq == nil {
|
||||
t.Fatal("expected external debit request")
|
||||
}
|
||||
if transferCalled {
|
||||
t.Fatal("expected external debit to skip transfer")
|
||||
}
|
||||
if got, want := postReq.GetMoney().GetAmount(), "76.5"; got != want {
|
||||
t.Fatalf("money.amount mismatch: got=%q want=%q", got, want)
|
||||
}
|
||||
if got, want := postReq.GetMoney().GetCurrency(), "RUB"; got != want {
|
||||
t.Fatalf("money.currency mismatch: got=%q want=%q", got, want)
|
||||
}
|
||||
if got, want := postReq.GetRole(), ledgerv1.AccountRole_ACCOUNT_ROLE_HOLD; got != want {
|
||||
t.Fatalf("role mismatch: got=%v want=%v", got, want)
|
||||
}
|
||||
if got, want := postReq.GetMetadata()[ledgerMetadataMode], "finalize_debit"; got != want {
|
||||
t.Fatalf("mode metadata mismatch: got=%q want=%q", got, want)
|
||||
}
|
||||
if got, want := out.StepExecution.State, agg.StepStateCompleted; got != want {
|
||||
t.Fatalf("state mismatch: got=%q want=%q", got, want)
|
||||
}
|
||||
if len(out.StepExecution.ExternalRefs) != 1 {
|
||||
t.Fatalf("expected one external ref, got=%d", len(out.StepExecution.ExternalRefs))
|
||||
}
|
||||
if got, want := out.StepExecution.ExternalRefs[0].Ref, "entry-ext-debit"; got != want {
|
||||
t.Fatalf("external ref value mismatch: got=%q want=%q", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGatewayLedgerExecutor_ExecuteLedger_FinalizeDebitUsesHoldToTransitAndSettlementAmount(t *testing.T) {
|
||||
orgID := bson.NewObjectID()
|
||||
payment := testLedgerExecutorPayment(orgID)
|
||||
|
||||
@@ -52,7 +52,7 @@ func (i *Imp) startDiscoveryAnnouncer(cfg *config, producer msg.Producer) {
|
||||
|
||||
announce := discovery.Announcement{
|
||||
Service: "PAYMENTS_QUOTATION",
|
||||
Operations: []string{"payment.quote", "payment.multiquote"},
|
||||
Operations: []string{discovery.OperationPaymentQuote, discovery.OperationPaymentMultiQuote},
|
||||
InvokeURI: invokeURI,
|
||||
Version: appversion.Create().Short(),
|
||||
}
|
||||
|
||||
@@ -197,18 +197,6 @@ func (r *managedWalletNetworkResolver) listDiscoveredGatewayCandidates(ctx conte
|
||||
return candidates, nil
|
||||
}
|
||||
|
||||
func managedWalletNetworkFromResponse(resp *chainv1.GetManagedWalletResponse) (string, error) {
|
||||
asset, err := managedWalletAssetFromResponse(resp)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
network := strings.ToUpper(strings.TrimSpace(asset.GetChain()))
|
||||
if network == "" || network == "UNSPECIFIED" {
|
||||
return "", merrors.NoData("managed wallet network is missing")
|
||||
}
|
||||
return network, nil
|
||||
}
|
||||
|
||||
func managedWalletAssetFromResponse(resp *chainv1.GetManagedWalletResponse) (*paymenttypes.Asset, error) {
|
||||
wallet := resp.GetWallet()
|
||||
if wallet == nil || wallet.GetAsset() == nil {
|
||||
|
||||
@@ -8,7 +8,3 @@ import (
|
||||
func railFromEndpoint(endpoint model.PaymentEndpoint, attrs map[string]string, isSource bool) (model.Rail, string, error) {
|
||||
return plan.RailFromEndpoint(endpoint, attrs, isSource)
|
||||
}
|
||||
|
||||
func resolveRouteNetwork(attrs map[string]string, sourceNetwork, destNetwork string) (string, error) {
|
||||
return plan.ResolveRouteNetwork(attrs, sourceNetwork, destNetwork)
|
||||
}
|
||||
|
||||
@@ -152,7 +152,8 @@ func (s *Service) buildPaymentQuote(ctx context.Context, orgRef string, req *quo
|
||||
|
||||
func (s *Service) quoteFees(ctx context.Context, orgRef string, req *quoteRequest, baseAmount *moneyv1.Money) (*feesv1.PrecomputeFeesResponse, error) {
|
||||
if !s.deps.fees.available() {
|
||||
return &feesv1.PrecomputeFeesResponse{}, nil
|
||||
s.logger.Warn("Fees precompute failed: fee engine unavailable")
|
||||
return nil, merrors.Internal("fees_precompute_failed")
|
||||
}
|
||||
intent := req.GetIntent()
|
||||
amount := cloneProtoMoney(baseAmount)
|
||||
@@ -188,7 +189,8 @@ func (s *Service) quoteFees(ctx context.Context, orgRef string, req *quoteReques
|
||||
|
||||
func (s *Service) quoteConversionFees(ctx context.Context, orgRef string, req *quoteRequest, baseAmount *moneyv1.Money) (*feesv1.PrecomputeFeesResponse, error) {
|
||||
if !s.deps.fees.available() {
|
||||
return &feesv1.PrecomputeFeesResponse{}, nil
|
||||
s.logger.Warn("Conversion fee precompute failed: fee engine unavailable")
|
||||
return nil, merrors.Internal("fees_precompute_failed")
|
||||
}
|
||||
intent := req.GetIntent()
|
||||
amount := cloneProtoMoney(baseAmount)
|
||||
|
||||
@@ -2,6 +2,7 @@ package quotation
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
@@ -152,9 +153,89 @@ func TestBuildPaymentQuote_DoesNotRequestConversionFeesForManagedWalletToLedger(
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildPaymentQuote_ReturnsErrorWhenFeeEngineUnavailable(t *testing.T) {
|
||||
svc := NewService(zap.NewNop(), nil)
|
||||
req := "eRequest{
|
||||
Meta: &sharedv1.RequestMeta{OrganizationRef: "org_1"},
|
||||
IdempotencyKey: "idem_1",
|
||||
Intent: testManagedWalletToCardIntent(),
|
||||
}
|
||||
|
||||
_, _, err := svc.buildPaymentQuote(context.Background(), "org_1", req)
|
||||
if err == nil {
|
||||
t.Fatalf("expected error")
|
||||
}
|
||||
if !errors.Is(err, merrors.ErrInternal) {
|
||||
t.Fatalf("expected internal error, got: %v", err)
|
||||
}
|
||||
if !strings.Contains(err.Error(), "fees_precompute_failed") {
|
||||
t.Fatalf("expected fees_precompute_failed error, got: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildPaymentQuote_ReturnsErrorWhenBaseFeePrecomputeFails(t *testing.T) {
|
||||
feeClient := &stubFeeEngineClient{
|
||||
precomputeErrByOrigin: map[string]error{
|
||||
"payments.orchestrator.quote": merrors.Internal("billing_fees_unreachable"),
|
||||
},
|
||||
}
|
||||
|
||||
svc := NewService(zap.NewNop(), nil, WithFeeEngine(feeClient, 0))
|
||||
req := "eRequest{
|
||||
Meta: &sharedv1.RequestMeta{OrganizationRef: "org_1"},
|
||||
IdempotencyKey: "idem_1",
|
||||
Intent: testManagedWalletToLedgerIntent(),
|
||||
}
|
||||
|
||||
_, _, err := svc.buildPaymentQuote(context.Background(), "org_1", req)
|
||||
if err == nil {
|
||||
t.Fatalf("expected error")
|
||||
}
|
||||
if !errors.Is(err, merrors.ErrInternal) {
|
||||
t.Fatalf("expected internal error, got: %v", err)
|
||||
}
|
||||
if !strings.Contains(err.Error(), "fees_precompute_failed") {
|
||||
t.Fatalf("expected fees_precompute_failed error, got: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildPaymentQuote_ReturnsErrorWhenConversionFeePrecomputeFails(t *testing.T) {
|
||||
feeClient := &stubFeeEngineClient{
|
||||
precomputeByOrigin: map[string]*feesv1.PrecomputeFeesResponse{
|
||||
"payments.orchestrator.quote": {
|
||||
Lines: []*feesv1.DerivedPostingLine{
|
||||
testFeeLine("1.00", "USDT"),
|
||||
},
|
||||
},
|
||||
},
|
||||
precomputeErrByOrigin: map[string]error{
|
||||
"payments.orchestrator.conversion_quote": merrors.Internal("billing_fees_unreachable"),
|
||||
},
|
||||
}
|
||||
|
||||
svc := NewService(zap.NewNop(), nil, WithFeeEngine(feeClient, 0))
|
||||
req := "eRequest{
|
||||
Meta: &sharedv1.RequestMeta{OrganizationRef: "org_1"},
|
||||
IdempotencyKey: "idem_1",
|
||||
Intent: testManagedWalletToCardIntent(),
|
||||
}
|
||||
|
||||
_, _, err := svc.buildPaymentQuote(context.Background(), "org_1", req)
|
||||
if err == nil {
|
||||
t.Fatalf("expected error")
|
||||
}
|
||||
if !errors.Is(err, merrors.ErrInternal) {
|
||||
t.Fatalf("expected internal error, got: %v", err)
|
||||
}
|
||||
if !strings.Contains(err.Error(), "fees_precompute_failed") {
|
||||
t.Fatalf("expected fees_precompute_failed error, got: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
type stubFeeEngineClient struct {
|
||||
precomputeByOrigin map[string]*feesv1.PrecomputeFeesResponse
|
||||
precomputeReqs []*feesv1.PrecomputeFeesRequest
|
||||
precomputeByOrigin map[string]*feesv1.PrecomputeFeesResponse
|
||||
precomputeErrByOrigin map[string]error
|
||||
precomputeReqs []*feesv1.PrecomputeFeesRequest
|
||||
}
|
||||
|
||||
func (s *stubFeeEngineClient) QuoteFees(context.Context, *feesv1.QuoteFeesRequest, ...grpc.CallOption) (*feesv1.QuoteFeesResponse, error) {
|
||||
@@ -177,6 +258,9 @@ func (s *stubFeeEngineClient) PrecomputeFees(_ context.Context, in *feesv1.Preco
|
||||
}
|
||||
|
||||
originType := strings.TrimSpace(in.GetIntent().GetOriginType())
|
||||
if err := s.precomputeErrByOrigin[originType]; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
resp, ok := s.precomputeByOrigin[originType]
|
||||
if !ok || resp == nil {
|
||||
return &feesv1.PrecomputeFeesResponse{}, nil
|
||||
|
||||
71
api/pkg/discovery/operations.go
Normal file
71
api/pkg/discovery/operations.go
Normal file
@@ -0,0 +1,71 @@
|
||||
package discovery
|
||||
|
||||
import "strings"
|
||||
|
||||
const (
|
||||
OperationDiscoveryLookup = "discovery.lookup"
|
||||
|
||||
OperationDocumentsBatchResolve = "documents.batch_resolve"
|
||||
OperationDocumentsGet = "documents.get"
|
||||
OperationFeeCalc = "fee.calc"
|
||||
OperationNotifySend = "notify.send"
|
||||
OperationFXQuote = "fx.quote"
|
||||
OperationFXIngest = "fx.ingest"
|
||||
|
||||
OperationPaymentInitiate = "payment.initiate"
|
||||
OperationPaymentQuote = "payment.quote"
|
||||
OperationPaymentMultiQuote = "payment.multiquote"
|
||||
OperationPaymentMethodsManage = "payment_methods.manage"
|
||||
OperationPaymentMethodsRead = "payment_methods.read"
|
||||
|
||||
OperationBalanceRead = "balance.read"
|
||||
|
||||
OperationLedgerDebit = "ledger.debit"
|
||||
OperationLedgerCredit = "ledger.credit"
|
||||
OperationLedgerTransfer = "ledger.transfer"
|
||||
OperationLedgerFX = "ledger.fx"
|
||||
OperationExternalDebit = "external.debit"
|
||||
OperationExternalCredit = "external.credit"
|
||||
|
||||
OperationSend = "send"
|
||||
OperationFee = "fee"
|
||||
OperationObserveConfirm = "observe.confirm"
|
||||
OperationFXConvert = "fx.convert"
|
||||
)
|
||||
|
||||
// NormalizeOperation canonicalizes an operation string for comparisons.
|
||||
func NormalizeOperation(value string) string {
|
||||
return strings.ToLower(strings.TrimSpace(value))
|
||||
}
|
||||
|
||||
// HasAnyOperation reports whether ops contains any of required operations.
|
||||
func HasAnyOperation(ops []string, required []string) bool {
|
||||
if len(required) == 0 {
|
||||
return true
|
||||
}
|
||||
for _, op := range ops {
|
||||
normalized := NormalizeOperation(op)
|
||||
if normalized == "" {
|
||||
continue
|
||||
}
|
||||
for _, target := range required {
|
||||
if normalized == NormalizeOperation(target) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// LedgerServiceOperations returns canonical operations announced by ledger.
|
||||
func LedgerServiceOperations() []string {
|
||||
return []string{
|
||||
OperationBalanceRead,
|
||||
OperationLedgerDebit,
|
||||
OperationLedgerCredit,
|
||||
OperationLedgerTransfer,
|
||||
OperationLedgerFX,
|
||||
OperationExternalCredit,
|
||||
OperationExternalDebit,
|
||||
}
|
||||
}
|
||||
@@ -104,14 +104,20 @@ func ExpandRailOperation(value string) []string {
|
||||
}
|
||||
|
||||
switch strings.ToLower(strings.TrimSpace(value)) {
|
||||
case OperationExternalDebit, "external_debit":
|
||||
return []string{RailOperationExternalDebit}
|
||||
case OperationExternalCredit, "external_credit":
|
||||
return []string{RailOperationExternalCredit}
|
||||
case "payin", "payin.crypto", "payin.fiat", "payin.card":
|
||||
return []string{RailOperationExternalDebit}
|
||||
case "payout", "payout.crypto", "payout.fiat", "payout.card":
|
||||
return []string{RailOperationExternalCredit, RailOperationSend}
|
||||
case "fee.send", "fees.send":
|
||||
case "fee.send", "fees.send", OperationFee:
|
||||
return []string{RailOperationFee}
|
||||
case "observe.confirm", "observe_confirm":
|
||||
case OperationObserveConfirm, "observe_confirm":
|
||||
return []string{RailOperationObserveConfirm}
|
||||
case OperationFXConvert, "fx_convert":
|
||||
return []string{RailOperationFXConvert}
|
||||
case "funds.block", "hold.balance", "block":
|
||||
return []string{RailOperationBlock}
|
||||
case "funds.release", "release", "unblock":
|
||||
@@ -147,6 +153,13 @@ func NormalizeRailOperations(values []string) []string {
|
||||
// CryptoRailGatewayOperations returns canonical operations for crypto rail gateways.
|
||||
func CryptoRailGatewayOperations() []string {
|
||||
return []string{
|
||||
OperationBalanceRead,
|
||||
OperationSend,
|
||||
OperationExternalDebit,
|
||||
OperationExternalCredit,
|
||||
OperationFee,
|
||||
OperationObserveConfirm,
|
||||
// Legacy rail tokens retained for backward compatibility.
|
||||
RailOperationSend,
|
||||
RailOperationExternalDebit,
|
||||
RailOperationExternalCredit,
|
||||
@@ -158,6 +171,10 @@ func CryptoRailGatewayOperations() []string {
|
||||
// CardPayoutRailGatewayOperations returns canonical operations for card payout gateways.
|
||||
func CardPayoutRailGatewayOperations() []string {
|
||||
return []string{
|
||||
OperationSend,
|
||||
OperationExternalCredit,
|
||||
OperationObserveConfirm,
|
||||
// Legacy rail tokens retained for backward compatibility.
|
||||
RailOperationSend,
|
||||
RailOperationExternalCredit,
|
||||
RailOperationObserveConfirm,
|
||||
@@ -167,6 +184,9 @@ func CardPayoutRailGatewayOperations() []string {
|
||||
// ProviderSettlementRailGatewayOperations returns canonical operations for settlement gateways.
|
||||
func ProviderSettlementRailGatewayOperations() []string {
|
||||
return []string{
|
||||
OperationFXConvert,
|
||||
OperationObserveConfirm,
|
||||
// Legacy rail tokens retained for backward compatibility.
|
||||
RailOperationFXConvert,
|
||||
RailOperationObserveConfirm,
|
||||
}
|
||||
|
||||
@@ -7,14 +7,16 @@ func TestNormalizeRailOperations(t *testing.T) {
|
||||
"send",
|
||||
"payout.crypto",
|
||||
"observe.confirm",
|
||||
"external.credit",
|
||||
"fx.convert",
|
||||
"unknown",
|
||||
"EXTERNAL_CREDIT",
|
||||
})
|
||||
|
||||
want := []string{
|
||||
RailOperationSend,
|
||||
RailOperationExternalCredit,
|
||||
RailOperationObserveConfirm,
|
||||
RailOperationFXConvert,
|
||||
}
|
||||
if len(got) != len(want) {
|
||||
t.Fatalf("unexpected operations count: got=%d want=%d", len(got), len(want))
|
||||
|
||||
@@ -19,15 +19,8 @@ import (
|
||||
const (
|
||||
discoveryBootstrapTimeout = 3 * time.Second
|
||||
discoveryBootstrapSender = "server_bootstrap"
|
||||
discoveryGatewayRailCrypto = "CRYPTO"
|
||||
defaultClientDialTimeoutSecs = 5
|
||||
defaultClientCallTimeoutSecs = 5
|
||||
paymentQuoteOperation = "payment.quote"
|
||||
paymentInitiateOperation = "payment.initiate"
|
||||
ledgerDebitOperation = "ledger.debit"
|
||||
ledgerCreditOperation = "ledger.credit"
|
||||
gatewayReadBalanceOperation = "balance.read"
|
||||
paymentMethodsReadOperation = "payment_methods.read"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -123,7 +116,7 @@ func (a *APIImp) resolveChainGatewayAddress(gateways []discovery.GatewaySummary)
|
||||
endpoint, selected, ok := selectGatewayEndpoint(
|
||||
gateways,
|
||||
cfg.DefaultAsset.Chain,
|
||||
[]string{gatewayReadBalanceOperation},
|
||||
[]string{discovery.OperationBalanceRead},
|
||||
)
|
||||
if !ok {
|
||||
return
|
||||
@@ -146,7 +139,7 @@ func (a *APIImp) resolveLedgerAddress(services []discovery.ServiceSummary) {
|
||||
endpoint, selected, ok := selectServiceEndpoint(
|
||||
services,
|
||||
ledgerDiscoveryServiceNames,
|
||||
[]string{ledgerDebitOperation, ledgerCreditOperation},
|
||||
discovery.LedgerServiceOperations(),
|
||||
)
|
||||
if !ok {
|
||||
return
|
||||
@@ -170,7 +163,7 @@ func (a *APIImp) resolvePaymentOrchestratorAddress(services []discovery.ServiceS
|
||||
endpoint, selected, ok := selectServiceEndpoint(
|
||||
services,
|
||||
paymentOrchestratorDiscoveryServiceNames,
|
||||
[]string{paymentInitiateOperation},
|
||||
[]string{discovery.OperationPaymentInitiate},
|
||||
)
|
||||
if !ok {
|
||||
return false, discoveryEndpoint{}
|
||||
@@ -196,7 +189,7 @@ func (a *APIImp) resolvePaymentQuotationAddress(services []discovery.ServiceSumm
|
||||
endpoint, selected, ok := selectServiceEndpoint(
|
||||
services,
|
||||
paymentQuotationDiscoveryServiceNames,
|
||||
[]string{paymentQuoteOperation},
|
||||
[]string{discovery.OperationPaymentQuote},
|
||||
)
|
||||
if !ok {
|
||||
cfg := a.config.PaymentQuotation
|
||||
@@ -229,7 +222,7 @@ func (a *APIImp) resolvePaymentMethodsAddress(services []discovery.ServiceSummar
|
||||
endpoint, selected, ok := selectServiceEndpoint(
|
||||
services,
|
||||
paymentMethodsDiscoveryServiceNames,
|
||||
[]string{paymentMethodsReadOperation},
|
||||
[]string{discovery.OperationPaymentMethodsRead},
|
||||
)
|
||||
if !ok {
|
||||
return
|
||||
@@ -269,7 +262,7 @@ func selectServiceEndpoint(services []discovery.ServiceSummary, serviceNames []s
|
||||
selections = append(selections, serviceSelection{
|
||||
service: svc,
|
||||
endpoint: endpoint,
|
||||
opMatch: hasAnyOperation(svc.Ops, requiredOps),
|
||||
opMatch: discovery.HasAnyOperation(svc.Ops, requiredOps),
|
||||
nameRank: nameRank,
|
||||
})
|
||||
}
|
||||
@@ -302,7 +295,7 @@ func selectGatewayEndpoint(gateways []discovery.GatewaySummary, preferredNetwork
|
||||
if !gateway.Healthy {
|
||||
continue
|
||||
}
|
||||
if !strings.EqualFold(strings.TrimSpace(gateway.Rail), discoveryGatewayRailCrypto) {
|
||||
if !strings.EqualFold(strings.TrimSpace(gateway.Rail), discovery.RailCrypto) {
|
||||
continue
|
||||
}
|
||||
if strings.TrimSpace(gateway.InvokeURI) == "" {
|
||||
@@ -316,7 +309,7 @@ func selectGatewayEndpoint(gateways []discovery.GatewaySummary, preferredNetwork
|
||||
gateway: gateway,
|
||||
endpoint: endpoint,
|
||||
networkMatch: preferredNetwork != "" && strings.EqualFold(strings.TrimSpace(gateway.Network), preferredNetwork),
|
||||
opMatch: hasAnyOperation(gateway.Ops, requiredOps),
|
||||
opMatch: discovery.HasAnyOperation(gateway.Ops, requiredOps),
|
||||
})
|
||||
}
|
||||
if len(selections) == 0 {
|
||||
@@ -412,24 +405,6 @@ func serviceRank(service string, names []string) (int, bool) {
|
||||
return 0, false
|
||||
}
|
||||
|
||||
func hasAnyOperation(ops []string, required []string) bool {
|
||||
if len(required) == 0 {
|
||||
return true
|
||||
}
|
||||
for _, op := range ops {
|
||||
normalized := strings.TrimSpace(op)
|
||||
if normalized == "" {
|
||||
continue
|
||||
}
|
||||
for _, target := range required {
|
||||
if strings.EqualFold(normalized, strings.TrimSpace(target)) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func ensureLedgerConfig(cfg *eapi.Config) *eapi.LedgerConfig {
|
||||
if cfg == nil {
|
||||
return nil
|
||||
|
||||
@@ -27,7 +27,7 @@ import (
|
||||
|
||||
const (
|
||||
documentsServiceName = "BILLING_DOCUMENTS"
|
||||
documentsOperationGet = "documents.get"
|
||||
documentsOperationGet = discovery.OperationDocumentsGet
|
||||
documentsDialTimeout = 5 * time.Second
|
||||
documentsCallTimeout = 10 * time.Second
|
||||
)
|
||||
|
||||
@@ -7,7 +7,7 @@ import 'package:pshared/models/payment/type.dart';
|
||||
import 'package:pweb/pages/dashboard/buttons/balance/add/asset_type_field.dart';
|
||||
import 'package:pweb/pages/dashboard/buttons/balance/add/description.dart';
|
||||
import 'package:pweb/pages/dashboard/buttons/balance/add/employees_loading_indicator.dart';
|
||||
import 'package:pweb/pages/dashboard/buttons/balance/add/ledger_fields.dart';
|
||||
import 'package:pweb/pages/dashboard/buttons/balance/add/ledger/fields.dart';
|
||||
import 'package:pweb/pages/dashboard/buttons/balance/add/managed_wallet_fields.dart';
|
||||
import 'package:pweb/pages/dashboard/buttons/balance/add/name.dart';
|
||||
import 'package:pweb/pages/dashboard/buttons/balance/add/owner.dart';
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:pshared/models/currency.dart';
|
||||
import 'package:pshared/utils/currency.dart';
|
||||
|
||||
|
||||
DropdownMenuItem<Currency> currencyItem(Currency currency) => DropdownMenuItem(
|
||||
value: currency,
|
||||
child: Text(currencyCodeToString(currency)),
|
||||
);
|
||||
@@ -1,9 +1,9 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:pshared/models/currency.dart';
|
||||
import 'package:pshared/utils/currency.dart';
|
||||
|
||||
import 'package:pweb/pages/dashboard/buttons/balance/add/constants.dart';
|
||||
import 'package:pweb/pages/dashboard/buttons/balance/add/ledger/currency_item.dart';
|
||||
import 'package:pweb/utils/text_field_styles.dart';
|
||||
|
||||
import 'package:pweb/generated/i18n/app_localizations.dart';
|
||||
@@ -24,10 +24,8 @@ class LedgerFields extends StatelessWidget {
|
||||
initialValue: currency,
|
||||
decoration: getInputDecoration(context, AppLocalizations.of(context)!.currency, true),
|
||||
items: [
|
||||
DropdownMenuItem(
|
||||
value: ledgerCurrencyDefault,
|
||||
child: Text(currencyCodeToString(ledgerCurrencyDefault)),
|
||||
),
|
||||
currencyItem(ledgerCurrencyDefault),
|
||||
currencyItem(managedCurrencyDefault),
|
||||
],
|
||||
onChanged: onCurrencyChanged,
|
||||
);
|
||||
Reference in New Issue
Block a user