fixed payment reference #571
@@ -58,7 +58,7 @@ func (e *gatewayCardPayoutExecutor) ExecuteCardPayout(ctx context.Context, req s
|
||||
|
||||
stepToken := cardPayoutStepToken(req.Step)
|
||||
operationRef := cardPayoutOperationRef(req.Payment, stepToken)
|
||||
payoutRef := cardPayoutRef(req.Payment, stepToken)
|
||||
payoutRef := cardPayoutRef(req.Payment)
|
||||
idempotencyKey := cardPayoutIdempotencyKey(req.Payment, stepToken)
|
||||
projectID := cardPayoutProjectID(req.Payment)
|
||||
customer := cardPayoutCustomerFromPayment(req.Payment, card, req.Step.Metadata)
|
||||
@@ -219,12 +219,12 @@ func cardPayoutOperationRef(payment *agg.Payment, stepToken string) string {
|
||||
return joinRef(base, stepToken)
|
||||
}
|
||||
|
||||
func cardPayoutRef(payment *agg.Payment, stepToken string) string {
|
||||
func cardPayoutRef(payment *agg.Payment) string {
|
||||
base := ""
|
||||
if payment != nil {
|
||||
base = firstNonEmpty(strings.TrimSpace(payment.PaymentRef), strings.TrimSpace(payment.IdempotencyKey), "card_payout")
|
||||
}
|
||||
return joinRef(base, stepToken)
|
||||
return base
|
||||
}
|
||||
|
||||
func cardPayoutIdempotencyKey(payment *agg.Payment, stepToken string) string {
|
||||
|
||||
@@ -104,7 +104,7 @@ func TestGatewayCardPayoutExecutor_ExecuteCardPayout_SubmitsCardPayout(t *testin
|
||||
if payoutReq == nil {
|
||||
t.Fatal("expected payout request to be submitted")
|
||||
}
|
||||
if got, want := payoutReq.GetPayoutId(), "payment-1:hop_4_card_payout_send"; got != want {
|
||||
if got, want := payoutReq.GetPayoutId(), "payment-1"; got != want {
|
||||
t.Fatalf("payout_id mismatch: got=%q want=%q", got, want)
|
||||
}
|
||||
if got, want := payoutReq.GetOperationRef(), "payment-1:hop_4_card_payout_send"; got != want {
|
||||
|
||||
@@ -3,6 +3,7 @@ package orchestrator
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"github.com/shopspring/decimal"
|
||||
"github.com/tech/sendico/pkg/discovery"
|
||||
"strings"
|
||||
|
||||
@@ -68,6 +69,10 @@ func (e *gatewayLedgerExecutor) ExecuteLedger(ctx context.Context, req sexec.Ste
|
||||
organizationRef := req.Payment.OrganizationRef.Hex()
|
||||
description := ledgerDescription(req.Step)
|
||||
metadata := ledgerTransferMetadata(req.Payment, req.Step, roles)
|
||||
charges, err := ledgerChargesForStep(req.Payment, req.Step, action, amount)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var resp *ledgerv1.PostResponse
|
||||
switch action {
|
||||
@@ -77,6 +82,7 @@ func (e *gatewayLedgerExecutor) ExecuteLedger(ctx context.Context, req sexec.Ste
|
||||
OrganizationRef: organizationRef,
|
||||
Money: amount,
|
||||
Description: description,
|
||||
Charges: charges,
|
||||
Metadata: metadata,
|
||||
Role: ledgerRoleToProto(roles.to),
|
||||
})
|
||||
@@ -86,6 +92,7 @@ func (e *gatewayLedgerExecutor) ExecuteLedger(ctx context.Context, req sexec.Ste
|
||||
OrganizationRef: organizationRef,
|
||||
Money: amount,
|
||||
Description: description,
|
||||
Charges: charges,
|
||||
Metadata: metadata,
|
||||
Role: ledgerRoleToProto(roles.from),
|
||||
})
|
||||
@@ -183,6 +190,111 @@ func settlementMoneyForLedger(payment *agg.Payment, source *paymenttypes.Money)
|
||||
return source
|
||||
}
|
||||
|
||||
func ledgerChargesForStep(payment *agg.Payment, step xplan.Step, action model.RailOperation, amount *moneyv1.Money) ([]*ledgerv1.PostingLine, error) {
|
||||
if !shouldAttachLedgerCharges(payment, step, action) {
|
||||
return nil, nil
|
||||
}
|
||||
if payment == nil || payment.QuoteSnapshot == nil {
|
||||
return nil, nil
|
||||
}
|
||||
currency := ""
|
||||
if amount != nil {
|
||||
currency = strings.TrimSpace(amount.GetCurrency())
|
||||
}
|
||||
return feeLinesToLedgerCharges(payment.QuoteSnapshot.FeeLines, currency)
|
||||
}
|
||||
|
||||
func shouldAttachLedgerCharges(payment *agg.Payment, step xplan.Step, action model.RailOperation) bool {
|
||||
switch action {
|
||||
case discovery.RailOperationExternalCredit, discovery.RailOperationExternalDebit:
|
||||
default:
|
||||
return false
|
||||
}
|
||||
|
||||
fromRail, toRail, ok := ledgerBoundaryRails(payment, step)
|
||||
if !ok {
|
||||
// Prefer attaching on external debits when boundary metadata is unavailable.
|
||||
return action == discovery.RailOperationExternalDebit
|
||||
}
|
||||
|
||||
switch action {
|
||||
case discovery.RailOperationExternalCredit:
|
||||
return isLedgerExternalRail(fromRail) && isLedgerInternalRail(toRail)
|
||||
case discovery.RailOperationExternalDebit:
|
||||
return isLedgerExternalRail(toRail)
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func feeLinesToLedgerCharges(lines []*paymenttypes.FeeLine, currency string) ([]*ledgerv1.PostingLine, error) {
|
||||
if len(lines) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
result := make([]*ledgerv1.PostingLine, 0, len(lines))
|
||||
for i := range lines {
|
||||
line := lines[i]
|
||||
if line == nil || line.GetMoney() == nil {
|
||||
continue
|
||||
}
|
||||
accountRef := strings.TrimSpace(line.GetLedgerAccountRef())
|
||||
if accountRef == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
lineCurrency := strings.TrimSpace(line.GetMoney().GetCurrency())
|
||||
if currency != "" && !strings.EqualFold(lineCurrency, currency) {
|
||||
continue
|
||||
}
|
||||
|
||||
amount, err := decimal.NewFromString(strings.TrimSpace(line.GetMoney().GetAmount()))
|
||||
if err != nil {
|
||||
return nil, merrors.InvalidArgument(fmt.Sprintf("ledger step: fee_lines[%d].money.amount is invalid", i))
|
||||
}
|
||||
amount = normalizeChargeAmountForSide(amount, line.GetSide())
|
||||
|
||||
result = append(result, &ledgerv1.PostingLine{
|
||||
LedgerAccountRef: accountRef,
|
||||
Money: &moneyv1.Money{
|
||||
Amount: amount.String(),
|
||||
Currency: lineCurrency,
|
||||
},
|
||||
LineType: feeLineTypeToLedger(line.GetLineType()),
|
||||
})
|
||||
}
|
||||
if len(result) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func normalizeChargeAmountForSide(amount decimal.Decimal, side paymenttypes.EntrySide) decimal.Decimal {
|
||||
switch side {
|
||||
case paymenttypes.EntrySideDebit:
|
||||
if amount.Sign() > 0 {
|
||||
return amount.Neg()
|
||||
}
|
||||
case paymenttypes.EntrySideCredit:
|
||||
if amount.Sign() < 0 {
|
||||
return amount.Neg()
|
||||
}
|
||||
}
|
||||
return amount
|
||||
}
|
||||
|
||||
func feeLineTypeToLedger(lineType paymenttypes.PostingLineType) ledgerv1.LineType {
|
||||
switch lineType {
|
||||
case paymenttypes.PostingLineTypeSpread:
|
||||
return ledgerv1.LineType_LINE_SPREAD
|
||||
case paymenttypes.PostingLineTypeReversal:
|
||||
return ledgerv1.LineType_LINE_REVERSAL
|
||||
default:
|
||||
// Ledger supports fee/spread/reversal only; map TAX/UNSPECIFIED to fee.
|
||||
return ledgerv1.LineType_LINE_FEE
|
||||
}
|
||||
}
|
||||
|
||||
func payoutMoneyForLedger(_ *agg.Payment, settlement *paymenttypes.Money) *paymenttypes.Money {
|
||||
return settlement
|
||||
}
|
||||
|
||||
@@ -222,6 +222,199 @@ func TestGatewayLedgerExecutor_ExecuteLedger_ExternalDebitUsesPostDebitWithCharg
|
||||
}
|
||||
}
|
||||
|
||||
func TestGatewayLedgerExecutor_ExecuteLedger_ExternalDebitForwardsQuoteFeeLinesAsCharges(t *testing.T) {
|
||||
orgID := bson.NewObjectID()
|
||||
payment := testLedgerExecutorPayment(orgID)
|
||||
payment.QuoteSnapshot.FeeLines = []*paymenttypes.FeeLine{
|
||||
{
|
||||
LedgerAccountRef: "fee-ledger-1",
|
||||
Money: &paymenttypes.Money{Amount: "2.50", Currency: "RUB"},
|
||||
LineType: paymenttypes.PostingLineTypeFee,
|
||||
Side: paymenttypes.EntrySideDebit,
|
||||
},
|
||||
{
|
||||
LedgerAccountRef: "spread-ledger-1",
|
||||
Money: &paymenttypes.Money{Amount: "1.25", Currency: "RUB"},
|
||||
LineType: paymenttypes.PostingLineTypeSpread,
|
||||
Side: paymenttypes.EntrySideCredit,
|
||||
},
|
||||
{
|
||||
LedgerAccountRef: "mismatch-ledger-1",
|
||||
Money: &paymenttypes.Money{Amount: "5", Currency: "USDT"},
|
||||
LineType: paymenttypes.PostingLineTypeFee,
|
||||
Side: paymenttypes.EntrySideDebit,
|
||||
},
|
||||
}
|
||||
|
||||
var postReq *ledgerv1.PostDebitRequest
|
||||
executor := &gatewayLedgerExecutor{
|
||||
ledgerClient: &ledgerclient.Fake{
|
||||
PostExternalDebitWithChargesFn: func(_ context.Context, req *ledgerv1.PostDebitRequest) (*ledgerv1.PostResponse, error) {
|
||||
postReq = req
|
||||
return &ledgerv1.PostResponse{JournalEntryRef: "entry-ext-debit-fees"}, nil
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
_, 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: discovery.RailOperationExternalDebit,
|
||||
Rail: discovery.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 postReq == nil {
|
||||
t.Fatal("expected external debit request")
|
||||
}
|
||||
if got, want := len(postReq.GetCharges()), 2; got != want {
|
||||
t.Fatalf("charges count mismatch: got=%d want=%d", got, want)
|
||||
}
|
||||
if got, want := postReq.GetCharges()[0].GetLedgerAccountRef(), "fee-ledger-1"; got != want {
|
||||
t.Fatalf("charges[0].ledger_account_ref mismatch: got=%q want=%q", got, want)
|
||||
}
|
||||
if got, want := postReq.GetCharges()[0].GetMoney().GetCurrency(), "RUB"; got != want {
|
||||
t.Fatalf("charges[0].money.currency mismatch: got=%q want=%q", got, want)
|
||||
}
|
||||
if got, want := postReq.GetCharges()[0].GetMoney().GetAmount(), "-2.5"; got != want {
|
||||
t.Fatalf("charges[0].money.amount mismatch: got=%q want=%q", got, want)
|
||||
}
|
||||
if got, want := postReq.GetCharges()[0].GetLineType(), ledgerv1.LineType_LINE_FEE; got != want {
|
||||
t.Fatalf("charges[0].line_type mismatch: got=%v want=%v", got, want)
|
||||
}
|
||||
if got, want := postReq.GetCharges()[1].GetLedgerAccountRef(), "spread-ledger-1"; got != want {
|
||||
t.Fatalf("charges[1].ledger_account_ref mismatch: got=%q want=%q", got, want)
|
||||
}
|
||||
if got, want := postReq.GetCharges()[1].GetMoney().GetAmount(), "1.25"; got != want {
|
||||
t.Fatalf("charges[1].money.amount mismatch: got=%q want=%q", got, want)
|
||||
}
|
||||
if got, want := postReq.GetCharges()[1].GetLineType(), ledgerv1.LineType_LINE_SPREAD; got != want {
|
||||
t.Fatalf("charges[1].line_type mismatch: got=%v want=%v", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGatewayLedgerExecutor_ExecuteLedger_ExternalCreditSkipsChargesForExternalToExternalBoundary(t *testing.T) {
|
||||
orgID := bson.NewObjectID()
|
||||
payment := testLedgerExecutorPayment(orgID)
|
||||
payment.QuoteSnapshot.FeeLines = []*paymenttypes.FeeLine{
|
||||
{
|
||||
LedgerAccountRef: "fee-ledger-1",
|
||||
Money: &paymenttypes.Money{Amount: "1.00", Currency: "USDT"},
|
||||
LineType: paymenttypes.PostingLineTypeFee,
|
||||
Side: paymenttypes.EntrySideDebit,
|
||||
},
|
||||
}
|
||||
|
||||
var postReq *ledgerv1.PostCreditRequest
|
||||
executor := &gatewayLedgerExecutor{
|
||||
ledgerClient: &ledgerclient.Fake{
|
||||
PostExternalCreditWithChargesFn: func(_ context.Context, req *ledgerv1.PostCreditRequest) (*ledgerv1.PostResponse, error) {
|
||||
postReq = req
|
||||
return &ledgerv1.PostResponse{JournalEntryRef: "entry-ext-credit-no-dup"}, nil
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
_, 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: discovery.RailOperationExternalCredit,
|
||||
Rail: discovery.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 postReq == nil {
|
||||
t.Fatal("expected external credit request")
|
||||
}
|
||||
if got, want := len(postReq.GetCharges()), 0; got != want {
|
||||
t.Fatalf("charges count mismatch: got=%d want=%d", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGatewayLedgerExecutor_ExecuteLedger_ExternalCreditForwardsChargesForExternalToInternalBoundary(t *testing.T) {
|
||||
orgID := bson.NewObjectID()
|
||||
payment := testLedgerExecutorPayment(orgID)
|
||||
payment.QuoteSnapshot.ExpectedSettlementAmount = &paymenttypes.Money{Amount: "10", Currency: "USD"}
|
||||
payment.QuoteSnapshot.Route = &paymenttypes.QuoteRouteSpecification{
|
||||
Hops: []*paymenttypes.QuoteRouteHop{
|
||||
{Index: 1, Rail: "CRYPTO", Role: paymenttypes.QuoteRouteHopRoleSource},
|
||||
{Index: 2, Rail: "LEDGER", Role: paymenttypes.QuoteRouteHopRoleTransit},
|
||||
},
|
||||
}
|
||||
payment.QuoteSnapshot.FeeLines = []*paymenttypes.FeeLine{
|
||||
{
|
||||
LedgerAccountRef: "fee-ledger-1",
|
||||
Money: &paymenttypes.Money{Amount: "0.75", Currency: "USD"},
|
||||
LineType: paymenttypes.PostingLineTypeTax, // mapped to LINE_FEE in ledger
|
||||
Side: paymenttypes.EntrySideDebit,
|
||||
},
|
||||
}
|
||||
|
||||
var postReq *ledgerv1.PostCreditRequest
|
||||
executor := &gatewayLedgerExecutor{
|
||||
ledgerClient: &ledgerclient.Fake{
|
||||
PostExternalCreditWithChargesFn: func(_ context.Context, req *ledgerv1.PostCreditRequest) (*ledgerv1.PostResponse, error) {
|
||||
postReq = req
|
||||
return &ledgerv1.PostResponse{JournalEntryRef: "entry-ext-credit-fees"}, nil
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
_, 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: discovery.RailOperationExternalCredit,
|
||||
Rail: discovery.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 postReq == nil {
|
||||
t.Fatal("expected external credit request")
|
||||
}
|
||||
if got, want := postReq.GetMoney().GetCurrency(), "USD"; got != want {
|
||||
t.Fatalf("money.currency mismatch: got=%q want=%q", got, want)
|
||||
}
|
||||
if got, want := len(postReq.GetCharges()), 1; got != want {
|
||||
t.Fatalf("charges count mismatch: got=%d want=%d", got, want)
|
||||
}
|
||||
if got, want := postReq.GetCharges()[0].GetMoney().GetAmount(), "-0.75"; got != want {
|
||||
t.Fatalf("charges[0].money.amount mismatch: got=%q want=%q", got, want)
|
||||
}
|
||||
if got, want := postReq.GetCharges()[0].GetLineType(), ledgerv1.LineType_LINE_FEE; got != want {
|
||||
t.Fatalf("charges[0].line_type mismatch: got=%v want=%v", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGatewayLedgerExecutor_ExecuteLedger_FinalizeDebitUsesHoldToTransitAndSettlementAmount(t *testing.T) {
|
||||
orgID := bson.NewObjectID()
|
||||
payment := testLedgerExecutorPayment(orgID)
|
||||
|
||||
Reference in New Issue
Block a user