propagated payment commentto bff

This commit is contained in:
Stephan D
2026-03-12 00:42:49 +01:00
parent 4958bdb500
commit b440df97d5
94 changed files with 626 additions and 119 deletions

View File

@@ -44,7 +44,7 @@ require (
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect
go.uber.org/multierr v1.11.0 // indirect
go.yaml.in/yaml/v2 v2.4.4 // indirect
golang.org/x/crypto v0.48.0 // indirect
golang.org/x/crypto v0.49.0 // indirect
golang.org/x/net v0.51.0 // indirect
golang.org/x/sync v0.20.0 // indirect
golang.org/x/sys v0.42.0 // indirect

View File

@@ -174,8 +174,8 @@ go.yaml.in/yaml/v2 v2.4.4 h1:tuyd0P+2Ont/d6e2rl3be67goVK4R6deVxCUX5vyPaQ=
go.yaml.in/yaml/v2 v2.4.4/go.mod h1:gMZqIpDtDqOfM0uNfy0SkpRhvUryYH0Z6wdMYcacYXQ=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=
golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos=
golang.org/x/crypto v0.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4=
golang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=

View File

@@ -59,7 +59,7 @@ require (
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect
go.uber.org/multierr v1.11.0 // indirect
go.yaml.in/yaml/v2 v2.4.4 // indirect
golang.org/x/crypto v0.48.0 // indirect
golang.org/x/crypto v0.49.0 // indirect
golang.org/x/net v0.51.0 // indirect
golang.org/x/sync v0.20.0 // indirect
golang.org/x/sys v0.42.0 // indirect

View File

@@ -175,8 +175,8 @@ go.yaml.in/yaml/v2 v2.4.4 h1:tuyd0P+2Ont/d6e2rl3be67goVK4R6deVxCUX5vyPaQ=
go.yaml.in/yaml/v2 v2.4.4/go.mod h1:gMZqIpDtDqOfM0uNfy0SkpRhvUryYH0Z6wdMYcacYXQ=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=
golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos=
golang.org/x/crypto v0.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4=
golang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=

View File

@@ -51,6 +51,7 @@ type StepShell struct {
InstanceID string `bson:"instanceId,omitempty" json:"instanceId,omitempty"`
ReportVisibility model.ReportVisibility `bson:"reportVisibility,omitempty" json:"reportVisibility,omitempty"`
UserLabel string `bson:"userLabel,omitempty" json:"userLabel,omitempty"`
Comment string `bson:"comment,omitempty" json:"comment,omitempty"`
PlannedMoney *paymenttypes.Money `bson:"plannedMoney,omitempty" json:"plannedMoney,omitempty"`
PlannedConvertedMoney *paymenttypes.Money `bson:"plannedConvertedMoney,omitempty" json:"plannedConvertedMoney,omitempty"`
}
@@ -64,6 +65,7 @@ type StepExecution struct {
InstanceID string `bson:"instanceId,omitempty" json:"instanceId,omitempty"`
ReportVisibility model.ReportVisibility `bson:"reportVisibility,omitempty" json:"reportVisibility,omitempty"`
UserLabel string `bson:"userLabel,omitempty" json:"userLabel,omitempty"`
Comment string `bson:"comment,omitempty" json:"comment,omitempty"`
State StepState `bson:"state" json:"state"`
Attempt uint32 `bson:"attempt" json:"attempt"`
StartedAt *time.Time `bson:"startedAt,omitempty" json:"startedAt,omitempty"`

View File

@@ -156,6 +156,7 @@ func buildInitialStepTelemetry(shell []StepShell) ([]StepExecution, error) {
InstanceID: instanceID,
ReportVisibility: visibility,
UserLabel: userLabel,
Comment: strings.TrimSpace(shell[i].Comment),
State: StepStatePending,
Attempt: 1,
PlannedMoney: svcshared.CloneMoneyTrimNonEmpty(shell[i].PlannedMoney),

View File

@@ -42,6 +42,7 @@ const (
type PayoutTarget struct {
TargetRef string `json:"target_ref,omitempty"`
IntentRef []string `json:"intent_refs,omitempty"`
Comment string `json:"comment,omitempty"`
Amount *paymenttypes.Money `json:"amount,omitempty"`
Card *model.CardEndpoint `json:"card,omitempty"`
Customer *model.Customer `json:"customer,omitempty"`
@@ -208,6 +209,7 @@ func normalizeTarget(target PayoutTarget, index int) PayoutTarget {
target.TargetRef = "target-" + strconv.Itoa(index+1)
}
target.IntentRef = normalizeStringSlice(target.IntentRef)
target.Comment = strings.TrimSpace(target.Comment)
if target.Amount != nil {
amount := strings.TrimSpace(target.Amount.Amount)
currency := strings.ToUpper(strings.TrimSpace(target.Amount.Currency))

View File

@@ -103,6 +103,7 @@ func cloneStepExecutions(src []agg.StepExecution) []agg.StepExecution {
step.ReportVisibility = model.ReportVisibilityUnspecified
}
step.UserLabel = strings.TrimSpace(step.UserLabel)
step.Comment = strings.TrimSpace(step.Comment)
step.FailureCode = strings.TrimSpace(step.FailureCode)
step.FailureMsg = strings.TrimSpace(step.FailureMsg)
if step.Attempt == 0 {

View File

@@ -529,6 +529,7 @@ func normalizePayment(payment *agg.Payment, requirePaymentRef bool) (*paymentDoc
step := &doc.StepExecutions[i]
step.StepRef = strings.TrimSpace(step.StepRef)
step.StepCode = strings.TrimSpace(step.StepCode)
step.Comment = strings.TrimSpace(step.Comment)
step.FailureCode = strings.TrimSpace(step.FailureCode)
step.FailureMsg = strings.TrimSpace(step.FailureMsg)
if step.StepRef == "" {

View File

@@ -36,7 +36,7 @@ func mapIntentSnapshot(src model.PaymentIntent) (*quotationv2.QuoteIntent, error
SettlementMode: settlementMode,
FeeTreatment: feeTreatment,
SettlementCurrency: strings.ToUpper(strings.TrimSpace(src.SettlementCurrency)),
Comment: strings.TrimSpace(src.Attributes["comment"]),
Comment: strings.TrimSpace(src.Comment),
}, nil
}

View File

@@ -271,9 +271,7 @@ func newPaymentFixture() *agg.Payment {
SettlementMode: model.SettlementModeFixSource,
FeeTreatment: model.FeeTreatmentDeductFromDestination,
SettlementCurrency: "USD",
Attributes: map[string]string{
"comment": "invoice-7",
},
Comment: "invoice-7",
},
QuoteSnapshot: &model.PaymentQuoteSnapshot{
DebitAmount: &paymenttypes.Money{

View File

@@ -228,6 +228,7 @@ func toStepShells(graph *xplan.Graph, intent model.PaymentIntent, quote *model.P
InstanceID: graph.Steps[i].InstanceID,
ReportVisibility: graph.Steps[i].Visibility,
UserLabel: graph.Steps[i].UserLabel,
Comment: graph.Steps[i].Comment,
PlannedMoney: plannedMoney,
PlannedConvertedMoney: plannedConverted,
})

View File

@@ -320,6 +320,7 @@ func buildBatchPayoutTargets(group opagg.Group) []batchmeta.PayoutTarget {
target := batchmeta.PayoutTarget{
TargetRef: firstNonEmpty(strings.TrimSpace(member.IntentRef), "recipient-"+strconv.Itoa(i+1)),
IntentRef: normalizeIntentRefs([]string{member.IntentRef}),
Comment: strings.TrimSpace(member.IntentSnapshot.Comment),
Amount: batchPayoutAmountFromMember(member),
}
if member.IntentSnapshot.Destination.Type == model.EndpointTypeCard && member.IntentSnapshot.Destination.Card != nil {
@@ -344,6 +345,7 @@ func buildBatchPayoutTargets(group opagg.Group) []batchmeta.PayoutTarget {
target := batchmeta.PayoutTarget{
TargetRef: firstNonEmpty(strings.TrimSpace(group.RecipientKey), "recipient-1"),
IntentRef: normalizeIntentRefs(group.IntentRefs),
Comment: strings.TrimSpace(group.IntentSnapshot.Comment),
Amount: batchPayoutAmount(group),
}
if group.IntentSnapshot.Destination.Type == model.EndpointTypeCard && group.IntentSnapshot.Destination.Card != nil {

View File

@@ -80,11 +80,13 @@ func TestExecuteBatchPayment_CardBatchCreatesPerTargetPayoutStepsWithoutMerging(
var (
targetRefs []string
amounts []string
comments []string
)
env := newTestEnv(t, func(kind string, req sexec.StepRequest) (*sexec.ExecuteOutput, error) {
if kind == "card_payout" {
targetRefs = append(targetRefs, req.Step.Metadata[batchmeta.MetaPayoutTargetRef])
amounts = append(amounts, req.Step.Metadata[batchmeta.MetaAmount])
comments = append(comments, req.Step.Comment)
}
step := req.StepExecution
step.State = agg.StepStateCompleted
@@ -116,15 +118,22 @@ func TestExecuteBatchPayment_CardBatchCreatesPerTargetPayoutStepsWithoutMerging(
if got, want := len(amounts), 2; got != want {
t.Fatalf("expected %d card payout amounts, got=%d", want, got)
}
if got, want := len(comments), 2; got != want {
t.Fatalf("expected %d card payout comments, got=%d", want, got)
}
sort.Strings(targetRefs)
sort.Strings(amounts)
sort.Strings(comments)
if targetRefs[0] == targetRefs[1] {
t.Fatalf("expected distinct target refs, got=%v", targetRefs)
}
if got, want := amounts, []string{"100", "150"}; got[0] != want[0] || got[1] != want[1] {
t.Fatalf("amount overrides mismatch: got=%v want=%v", got, want)
}
if got, want := comments, []string{"batch-comment-a", "batch-comment-b"}; got[0] != want[0] || got[1] != want[1] {
t.Fatalf("comment overrides mismatch: got=%v want=%v", got, want)
}
}
func TestExecuteBatchPayment_CryptoPolicyMergesByDestination(t *testing.T) {
@@ -343,9 +352,10 @@ func newExecutableBatchCardQuoteDiffDest(orgRef bson.ObjectID, quoteRef string)
Items: []*model.PaymentQuoteItemV2{
{
Intent: &model.PaymentIntent{
Ref: "intent-a",
Kind: model.PaymentKindPayout,
Source: testLedgerEndpoint("ledger-src"),
Ref: "intent-a",
Kind: model.PaymentKindPayout,
Comment: "batch-comment-a",
Source: testLedgerEndpoint("ledger-src"),
Destination: model.PaymentEndpoint{
Type: model.EndpointTypeCard,
Card: &model.CardEndpoint{
@@ -372,9 +382,10 @@ func newExecutableBatchCardQuoteDiffDest(orgRef bson.ObjectID, quoteRef string)
},
{
Intent: &model.PaymentIntent{
Ref: "intent-b",
Kind: model.PaymentKindPayout,
Source: testLedgerEndpoint("ledger-src"),
Ref: "intent-b",
Kind: model.PaymentKindPayout,
Comment: "batch-comment-b",
Source: testLedgerEndpoint("ledger-src"),
Destination: model.PaymentEndpoint{
Type: model.EndpointTypeCard,
Card: &model.CardEndpoint{

View File

@@ -325,6 +325,7 @@ func normalizeExecutorOutput(current agg.StepExecution, out *sexec.ExecuteOutput
if out.StepExecution.Attempt != 0 {
next.Attempt = out.StepExecution.Attempt
}
next.Comment = strings.TrimSpace(out.StepExecution.Comment)
next.ExternalRefs = out.StepExecution.ExternalRefs
next.ExecutedMoney = svcshared.CloneMoneyTrimNonEmpty(out.StepExecution.ExecutedMoney)
next.ConvertedMoney = svcshared.CloneMoneyTrimNonEmpty(out.StepExecution.ConvertedMoney)
@@ -431,6 +432,9 @@ func stepExecutionEqual(left, right agg.StepExecution) bool {
if strings.TrimSpace(left.FailureMsg) != strings.TrimSpace(right.FailureMsg) {
return false
}
if strings.TrimSpace(left.Comment) != strings.TrimSpace(right.Comment) {
return false
}
if !timePtrEqual(left.StartedAt, right.StartedAt) || !timePtrEqual(left.CompletedAt, right.CompletedAt) {
return false
}

View File

@@ -114,6 +114,7 @@ func validateInput(in ExecuteInput) (StepRequest, error) {
func normalizeStep(step xplan.Step) (xplan.Step, error) {
step.StepRef = strings.TrimSpace(step.StepRef)
step.StepCode = strings.TrimSpace(step.StepCode)
step.Comment = strings.TrimSpace(step.Comment)
if step.StepRef == "" {
return xplan.Step{}, merrors.InvalidArgument("step.step_ref is required")
}
@@ -126,6 +127,7 @@ func normalizeStep(step xplan.Step) (xplan.Step, error) {
func normalizeStepExecution(exec agg.StepExecution, step xplan.Step) (agg.StepExecution, error) {
exec.StepRef = strings.TrimSpace(exec.StepRef)
exec.StepCode = strings.TrimSpace(exec.StepCode)
exec.Comment = strings.TrimSpace(exec.Comment)
if exec.StepRef == "" {
exec.StepRef = step.StepRef
}

View File

@@ -64,6 +64,7 @@ func (s *svc) prepareInput(in Input) (*preparedInput, error) {
func normalizeGraphStep(step xplan.Step, index int) (xplan.Step, error) {
step.StepRef = strings.TrimSpace(step.StepRef)
step.StepCode = strings.TrimSpace(step.StepCode)
step.Comment = strings.TrimSpace(step.Comment)
if step.StepRef == "" {
return xplan.Step{}, merrors.InvalidArgument("steps[" + itoa(index) + "].step_ref is required")
}
@@ -157,6 +158,9 @@ func (s *svc) normalizeStepExecutions(
if strings.TrimSpace(exec.InstanceID) == "" {
exec.InstanceID = strings.TrimSpace(step.InstanceID)
}
if strings.TrimSpace(exec.Comment) == "" {
exec.Comment = strings.TrimSpace(step.Comment)
}
exec.ReportVisibility = effectiveStepVisibility(exec.ReportVisibility, stepsByRef[stepRef].Visibility)
exec.UserLabel = firstNonEmpty(exec.UserLabel, stepsByRef[stepRef].UserLabel)
cloned := cloneStepExecution(exec)
@@ -168,6 +172,7 @@ func (s *svc) normalizeStepExecutions(
func (s *svc) normalizeStepExecution(exec agg.StepExecution, index int) (agg.StepExecution, error) {
exec.StepRef = strings.TrimSpace(exec.StepRef)
exec.StepCode = strings.TrimSpace(exec.StepCode)
exec.Comment = strings.TrimSpace(exec.Comment)
exec.FailureCode = strings.TrimSpace(exec.FailureCode)
exec.FailureMsg = strings.TrimSpace(exec.FailureMsg)
exec.UserLabel = strings.TrimSpace(exec.UserLabel)
@@ -220,6 +225,7 @@ func seedMissingExecutions(
Rail: step.Rail,
Gateway: strings.TrimSpace(step.Gateway),
InstanceID: strings.TrimSpace(step.InstanceID),
Comment: strings.TrimSpace(step.Comment),
ReportVisibility: effectiveStepVisibility(model.ReportVisibilityUnspecified, step.Visibility),
UserLabel: strings.TrimSpace(step.UserLabel),
State: agg.StepStatePending,

View File

@@ -14,11 +14,13 @@ func TestCompile_InternalToCard_WithBatchTargetsExpandsPerRecipientPayoutBranche
targetsRaw, err := batchmeta.EncodePayoutTargets([]batchmeta.PayoutTarget{
{
TargetRef: "recipient-1",
Comment: "batch-comment-1",
Amount: &paymenttypes.Money{Amount: "100", Currency: "RUB"},
Card: &model.CardEndpoint{Pan: "2200700142860161", ExpMonth: 3, ExpYear: 2030},
},
{
TargetRef: "recipient-2",
Comment: "batch-comment-2",
Amount: &paymenttypes.Money{Amount: "150", Currency: "RUB"},
Card: &model.CardEndpoint{Pan: "2200700142860162", ExpMonth: 4, ExpYear: 2030},
},
@@ -69,6 +71,7 @@ func TestCompile_InternalToCard_WithBatchTargetsExpandsPerRecipientPayoutBranche
}
seenTargets := map[string]string{}
seenComments := map[string]string{}
for i := range sendSteps {
step := sendSteps[i]
if got, want := step.DependsOn, []string{graph.Steps[0].StepRef}; !equalStringSlice(got, want) {
@@ -76,10 +79,12 @@ func TestCompile_InternalToCard_WithBatchTargetsExpandsPerRecipientPayoutBranche
}
targetRef := step.Metadata[batchmeta.MetaPayoutTargetRef]
amount := step.Metadata[batchmeta.MetaAmount]
if targetRef == "" || amount == "" {
t.Fatalf("expected target metadata on send step, got=%v", step.Metadata)
comment := step.Comment
if targetRef == "" || amount == "" || comment == "" {
t.Fatalf("expected target fields on send step, metadata=%v comment=%q", step.Metadata, step.Comment)
}
seenTargets[targetRef] = amount
seenComments[targetRef] = comment
}
if got, want := seenTargets["recipient-1"], "100"; got != want {
t.Fatalf("recipient-1 amount mismatch: got=%q want=%q", got, want)
@@ -87,4 +92,10 @@ func TestCompile_InternalToCard_WithBatchTargetsExpandsPerRecipientPayoutBranche
if got, want := seenTargets["recipient-2"], "150"; got != want {
t.Fatalf("recipient-2 amount mismatch: got=%q want=%q", got, want)
}
if got, want := seenComments["recipient-1"], "batch-comment-1"; got != want {
t.Fatalf("recipient-1 comment mismatch: got=%q want=%q", got, want)
}
if got, want := seenComments["recipient-2"], "batch-comment-2"; got != want {
t.Fatalf("recipient-2 comment mismatch: got=%q want=%q", got, want)
}
}

View File

@@ -81,6 +81,7 @@ func normalizeStep(step Step) Step {
step.Gateway = strings.TrimSpace(step.Gateway)
step.InstanceID = strings.TrimSpace(step.InstanceID)
step.UserLabel = strings.TrimSpace(step.UserLabel)
step.Comment = strings.TrimSpace(step.Comment)
step.Visibility = model.NormalizeReportVisibility(step.Visibility)
step.CommitPolicy = normalizeCommitPolicy(step.CommitPolicy)
step.DependsOn = normalizeStringList(step.DependsOn)

View File

@@ -21,12 +21,12 @@ func planFeeHops(
return nil, err
}
if !hasFee {
return nil, nil
return map[string]struct{}{}, nil
}
sourceHop, ok := sourceFeeHop(hops)
if !ok {
return nil, nil
return map[string]struct{}{}, nil
}
return map[string]struct{}{
observedKey(sourceHop): {},

View File

@@ -56,6 +56,7 @@ type Step struct {
HopRole paymenttypes.QuoteRouteHopRole
Visibility model.ReportVisibility
UserLabel string
Comment string
CommitPolicy model.CommitPolicy
CommitAfter []string
Metadata map[string]string

View File

@@ -123,14 +123,17 @@ func (s *svc) applyBatchCardPayoutBoundary(
return merrors.InvalidArgument("intent_snapshot.attributes[" + batchmeta.AttrPayoutTargets + "]: card is required for each target")
}
targetMeta := batchmeta.StepMetadataForTarget(target, i)
targetComment := strings.TrimSpace(target.Comment)
sendStep := makeRailSendStep(to, intent)
sendStep.DependsOn = []string{blockRef}
sendStep.Comment = targetComment
sendStep.Metadata = cloneMetadata(targetMeta)
sendRef := ex.appendBranch(sendStep)
observeStep := makeRailObserveStep(to, intent)
observeStep.DependsOn = []string{sendRef}
observeStep.Comment = targetComment
observeStep.Metadata = cloneMetadata(targetMeta)
observeRef := ex.appendBranch(observeStep)

View File

@@ -79,6 +79,7 @@ func (e *gatewayCardPayoutExecutor) ExecuteCardPayout(ctx context.Context, req s
customer := cardPayoutCustomerFromPayment(req.Payment, card, req.Step.Metadata)
cardHolder := cardPayoutCardholder(card, customer)
metadata := cardPayoutMetadata(req.Payment, req.Step)
comment := destinationOperationComment(req.Payment, req.Step)
intentRef := strings.TrimSpace(req.Payment.IntentSnapshot.Ref)
var responsePayout *mntxv1.CardPayoutState
@@ -102,6 +103,7 @@ func (e *gatewayCardPayoutExecutor) ExecuteCardPayout(ctx context.Context, req s
CardHolder: cardHolder,
MaskedPan: strings.TrimSpace(card.MaskedPan),
Metadata: metadata,
Comment: comment,
OperationRef: operationRef,
IdempotencyKey: idempotencyKey,
IntentRef: intentRef,
@@ -141,6 +143,7 @@ func (e *gatewayCardPayoutExecutor) ExecuteCardPayout(ctx context.Context, req s
CardExpMonth: card.ExpMonth,
CardHolder: cardHolder,
Metadata: metadata,
Comment: comment,
OperationRef: operationRef,
IdempotencyKey: idempotencyKey,
IntentRef: intentRef,
@@ -177,6 +180,7 @@ func (e *gatewayCardPayoutExecutor) ExecuteCardPayout(ctx context.Context, req s
step.ConvertedMoney = nil
step.FailureCode = ""
step.FailureMsg = ""
assignDestinationStepComment(&step, req.Payment, req.Step)
return &sexec.ExecuteOutput{StepExecution: step}, nil
}

View File

@@ -58,7 +58,8 @@ func TestGatewayCardPayoutExecutor_ExecuteCardPayout_SubmitsCardPayout(t *testin
IdempotencyKey: "idem-1",
QuotationRef: "quote-1",
IntentSnapshot: model.PaymentIntent{
Ref: "intent-1",
Ref: "intent-1",
Comment: "invoice-1",
Destination: model.PaymentEndpoint{
Type: model.EndpointTypeCard,
Card: &model.CardEndpoint{
@@ -98,6 +99,7 @@ func TestGatewayCardPayoutExecutor_ExecuteCardPayout_SubmitsCardPayout(t *testin
Rail: discovery.RailCardPayout,
Gateway: paymenttypes.DefaultCardsGatewayID,
InstanceID: paymenttypes.DefaultCardsGatewayID,
HopRole: paymenttypes.QuoteRouteHopRoleDestination,
},
StepExecution: agg.StepExecution{
StepRef: "hop_4_card_payout_send",
@@ -156,6 +158,15 @@ func TestGatewayCardPayoutExecutor_ExecuteCardPayout_SubmitsCardPayout(t *testin
if got, want := payoutReq.GetMetadata()[settlementMetadataOutgoingLeg], string(discovery.RailCardPayout); got != want {
t.Fatalf("outgoing_leg metadata mismatch: got=%q want=%q", got, want)
}
if got, want := payoutReq.GetComment(), "invoice-1"; got != want {
t.Fatalf("comment mismatch: got=%q want=%q", got, want)
}
if _, ok := payoutReq.GetMetadata()["comment"]; ok {
t.Fatalf("comment must not be sent in metadata")
}
if got, want := out.StepExecution.Comment, "invoice-1"; got != want {
t.Fatalf("step comment mismatch: got=%q want=%q", got, want)
}
if got, want := payoutReq.GetParentPaymentRef(), "payment-1"; got != want {
t.Fatalf("parent_payment_ref mismatch: got=%q want=%q", got, want)
}
@@ -324,6 +335,7 @@ func TestGatewayCardPayoutExecutor_ExecuteCardPayout_UsesStepMetadataOverrides(t
Rail: discovery.RailCardPayout,
Gateway: paymenttypes.DefaultCardsGatewayID,
InstanceID: paymenttypes.DefaultCardsGatewayID,
Comment: "batch-note-2",
Metadata: map[string]string{
batchmeta.MetaPayoutTargetRef: "recipient-2",
batchmeta.MetaAmount: "150",
@@ -332,6 +344,7 @@ func TestGatewayCardPayoutExecutor_ExecuteCardPayout_UsesStepMetadataOverrides(t
batchmeta.MetaCardExpMonth: "4",
batchmeta.MetaCardExpYear: "2030",
},
HopRole: paymenttypes.QuoteRouteHopRoleDestination,
},
StepExecution: agg.StepExecution{
StepRef: "hop_4_card_payout_send",
@@ -368,6 +381,12 @@ func TestGatewayCardPayoutExecutor_ExecuteCardPayout_UsesStepMetadataOverrides(t
if got, want := payoutReq.GetMetadata()[batchmeta.MetaPayoutTargetRef], "recipient-2"; got != want {
t.Fatalf("target_ref metadata mismatch: got=%q want=%q", got, want)
}
if got, want := payoutReq.GetComment(), "batch-note-2"; got != want {
t.Fatalf("comment mismatch: got=%q want=%q", got, want)
}
if _, ok := payoutReq.GetMetadata()["comment"]; ok {
t.Fatalf("comment must not be sent in metadata")
}
if got, want := payoutReq.GetParentPaymentRef(), "payment-2"; got != want {
t.Fatalf("parent_payment_ref mismatch: got=%q want=%q", got, want)
}
@@ -450,7 +469,8 @@ func TestGatewayCardPayoutExecutor_ExecuteCardPayout_BatchChildrenUseDistinctOpe
IdempotencyKey: "idem-3",
QuotationRef: "quote-3",
IntentSnapshot: model.PaymentIntent{
Ref: "intent-3",
Ref: "intent-3",
Comment: "group-default-comment",
Destination: model.PaymentEndpoint{
Type: model.EndpointTypeCard,
Card: &model.CardEndpoint{
@@ -488,6 +508,7 @@ func TestGatewayCardPayoutExecutor_ExecuteCardPayout_BatchChildrenUseDistinctOpe
Rail: discovery.RailCardPayout,
Gateway: paymenttypes.DefaultCardsGatewayID,
InstanceID: paymenttypes.DefaultCardsGatewayID,
HopRole: paymenttypes.QuoteRouteHopRoleDestination,
},
StepExecution: agg.StepExecution{
StepRef: "hop_4_card_payout_send",
@@ -504,6 +525,7 @@ func TestGatewayCardPayoutExecutor_ExecuteCardPayout_BatchChildrenUseDistinctOpe
Rail: discovery.RailCardPayout,
Gateway: paymenttypes.DefaultCardsGatewayID,
InstanceID: paymenttypes.DefaultCardsGatewayID,
Comment: "recipient-2-comment",
Metadata: map[string]string{
batchmeta.MetaPayoutTargetRef: "recipient-2",
batchmeta.MetaAmount: "150",
@@ -512,6 +534,7 @@ func TestGatewayCardPayoutExecutor_ExecuteCardPayout_BatchChildrenUseDistinctOpe
batchmeta.MetaCardExpMonth: "4",
batchmeta.MetaCardExpYear: "2030",
},
HopRole: paymenttypes.QuoteRouteHopRoleDestination,
},
StepExecution: agg.StepExecution{
StepRef: "hop_4_card_payout_send_2",
@@ -545,6 +568,18 @@ func TestGatewayCardPayoutExecutor_ExecuteCardPayout_BatchChildrenUseDistinctOpe
if got, want := payoutReqs[1].GetParentPaymentRef(), "payment-3"; got != want {
t.Fatalf("second parent_payment_ref mismatch: got=%q want=%q", got, want)
}
if got, want := payoutReqs[0].GetComment(), "group-default-comment"; got != want {
t.Fatalf("first comment mismatch: got=%q want=%q", got, want)
}
if got, want := payoutReqs[1].GetComment(), "recipient-2-comment"; got != want {
t.Fatalf("second comment mismatch: got=%q want=%q", got, want)
}
if _, ok := payoutReqs[0].GetMetadata()["comment"]; ok {
t.Fatalf("first comment must not be sent in metadata")
}
if _, ok := payoutReqs[1].GetMetadata()["comment"]; ok {
t.Fatalf("second comment must not be sent in metadata")
}
if payoutReqs[0].GetCardPan() == payoutReqs[1].GetCardPan() {
t.Fatalf("expected different destination cards across child operations")
}

View File

@@ -102,6 +102,7 @@ func (e *gatewayCryptoExecutor) ExecuteCrypto(ctx context.Context, req sexec.Ste
step.State = agg.StepStateCompleted
step.FailureCode = ""
step.FailureMsg = ""
assignDestinationStepComment(&step, req.Payment, req.Step)
return &sexec.ExecuteOutput{StepExecution: step}, nil
}

View File

@@ -138,6 +138,88 @@ func TestGatewayCryptoExecutor_ExecuteCrypto_SubmitsTransfer(t *testing.T) {
}
}
func TestGatewayCryptoExecutor_ExecuteCrypto_AttachesCommentForDestinationHop(t *testing.T) {
orgID := bson.NewObjectID()
var submitReq *chainv1.SubmitTransferRequest
client := &chainclient.Fake{
SubmitTransferFn: func(_ context.Context, req *chainv1.SubmitTransferRequest) (*chainv1.SubmitTransferResponse, error) {
submitReq = req
return &chainv1.SubmitTransferResponse{
Transfer: &chainv1.Transfer{
TransferRef: "trf-comment",
OperationRef: "op-comment",
},
}, nil
},
}
executor := &gatewayCryptoExecutor{
gatewayInvokeResolver: &fakeGatewayInvokeResolver{client: client},
gatewayRegistry: &fakeGatewayRegistry{
items: []*model.GatewayInstanceDescriptor{
{
ID: "crypto_rail_gateway_arbitrum_sepolia",
InstanceID: "crypto_rail_gateway_arbitrum_sepolia",
Rail: discovery.RailCrypto,
InvokeURI: "grpc://crypto-gateway",
IsEnabled: true,
},
},
},
}
out, err := executor.ExecuteCrypto(context.Background(), sexec.StepRequest{
Payment: &agg.Payment{
OrganizationBoundBase: pm.OrganizationBoundBase{OrganizationRef: orgID},
PaymentRef: "payment-comment",
IdempotencyKey: "idem-comment",
IntentSnapshot: model.PaymentIntent{
Ref: "intent-comment",
Comment: "invoice-crypto-7",
Source: model.PaymentEndpoint{
Type: model.EndpointTypeManagedWallet,
ManagedWallet: &model.ManagedWalletEndpoint{
ManagedWalletRef: "wallet-src",
},
},
Destination: model.PaymentEndpoint{
Type: model.EndpointTypeExternalChain,
ExternalChain: &model.ExternalChainEndpoint{
Address: "TXYZdst",
},
},
Amount: &paymenttypes.Money{Amount: "1", Currency: "USDT"},
},
},
Step: xplan.Step{
StepRef: "hop_2_crypto_send",
StepCode: "hop.2.crypto.send",
Action: discovery.RailOperationSend,
Rail: discovery.RailCrypto,
Gateway: "crypto_rail_gateway_arbitrum_sepolia",
InstanceID: "crypto_rail_gateway_arbitrum_sepolia",
HopRole: paymenttypes.QuoteRouteHopRoleDestination,
},
StepExecution: agg.StepExecution{
StepRef: "hop_2_crypto_send",
StepCode: "hop.2.crypto.send",
Attempt: 1,
},
})
if err != nil {
t.Fatalf("ExecuteCrypto returned error: %v", err)
}
if submitReq == nil {
t.Fatal("expected transfer submission request")
}
if out == nil {
t.Fatal("expected output")
}
if got, want := out.StepExecution.Comment, "invoice-crypto-7"; got != want {
t.Fatalf("step comment mismatch: got=%q want=%q", got, want)
}
}
func TestGatewayCryptoExecutor_ExecuteCrypto_UsesPlannedMoney(t *testing.T) {
orgID := bson.NewObjectID()

View File

@@ -0,0 +1,29 @@
package orchestrator
import (
"strings"
"github.com/tech/sendico/payments/orchestrator/internal/service/orchestrationv2/agg"
"github.com/tech/sendico/payments/orchestrator/internal/service/orchestrationv2/xplan"
paymenttypes "github.com/tech/sendico/pkg/payments/types"
)
func destinationOperationComment(payment *agg.Payment, step xplan.Step) string {
if step.HopRole != paymenttypes.QuoteRouteHopRoleDestination {
return ""
}
if comment := strings.TrimSpace(step.Comment); comment != "" {
return comment
}
if payment == nil {
return ""
}
return strings.TrimSpace(payment.IntentSnapshot.Comment)
}
func assignDestinationStepComment(stepExecution *agg.StepExecution, payment *agg.Payment, step xplan.Step) {
if stepExecution == nil {
return
}
stepExecution.Comment = destinationOperationComment(payment, step)
}

View File

@@ -0,0 +1,80 @@
package orchestrator
import (
"testing"
"github.com/tech/sendico/payments/orchestrator/internal/service/orchestrationv2/agg"
"github.com/tech/sendico/payments/orchestrator/internal/service/orchestrationv2/xplan"
"github.com/tech/sendico/payments/storage/model"
paymenttypes "github.com/tech/sendico/pkg/payments/types"
)
func TestDestinationOperationComment_AssignsCommentByDestinationHopRole(t *testing.T) {
comment := destinationOperationComment(
&agg.Payment{IntentSnapshot: model.PaymentIntent{Comment: " invoice-7 "}},
xplan.Step{HopRole: paymenttypes.QuoteRouteHopRoleDestination},
)
if got, want := comment, "invoice-7"; got != want {
t.Fatalf("comment mismatch: got=%q want=%q", got, want)
}
}
func TestDestinationOperationComment_DoesNotAssignForNonDestinationHop(t *testing.T) {
comment := destinationOperationComment(
&agg.Payment{IntentSnapshot: model.PaymentIntent{Comment: "invoice-7"}},
xplan.Step{HopRole: paymenttypes.QuoteRouteHopRoleSource},
)
if comment != "" {
t.Fatalf("comment must not be set for non-destination hop")
}
}
func TestDestinationOperationComment_PrefersPerTargetComment(t *testing.T) {
comment := destinationOperationComment(
&agg.Payment{IntentSnapshot: model.PaymentIntent{Comment: "payment-comment"}},
xplan.Step{
HopRole: paymenttypes.QuoteRouteHopRoleDestination,
Comment: " recipient-comment ",
},
)
if got, want := comment, "recipient-comment"; got != want {
t.Fatalf("comment mismatch: got=%q want=%q", got, want)
}
}
func TestAssignDestinationStepComment_AssignsForDestinationHop(t *testing.T) {
stepExecution := agg.StepExecution{}
assignDestinationStepComment(
&stepExecution,
&agg.Payment{IntentSnapshot: model.PaymentIntent{Comment: " invoice-8 "}},
xplan.Step{
StepRef: "hop_4",
StepCode: "hop.4.card.send",
HopRole: paymenttypes.QuoteRouteHopRoleDestination,
},
)
if got, want := stepExecution.Comment, "invoice-8"; got != want {
t.Fatalf("step comment mismatch: got=%q want=%q", got, want)
}
}
func TestAssignDestinationStepComment_ClearsForNonDestinationHop(t *testing.T) {
stepExecution := agg.StepExecution{Comment: "old-value"}
assignDestinationStepComment(
&stepExecution,
&agg.Payment{IntentSnapshot: model.PaymentIntent{Comment: "invoice-8"}},
xplan.Step{
StepRef: "hop_1",
StepCode: "hop.1.crypto.send",
HopRole: paymenttypes.QuoteRouteHopRoleSource,
},
)
if got := stepExecution.Comment; got != "" {
t.Fatalf("step comment must be empty for non-destination hop, got=%q", got)
}
}

View File

@@ -104,6 +104,7 @@ func (e *gatewayProviderSettlementExecutor) ExecuteProviderSettlement(ctx contex
step.State = agg.StepStateCompleted
step.FailureCode = ""
step.FailureMsg = ""
assignDestinationStepComment(&step, req.Payment, req.Step)
return &sexec.ExecuteOutput{StepExecution: step}, nil
}

View File

@@ -241,6 +241,87 @@ func TestGatewayProviderSettlementExecutor_ExecuteProviderSettlement_UsesPlanned
}
}
func TestGatewayProviderSettlementExecutor_ExecuteProviderSettlement_AssignsDestinationComment(t *testing.T) {
orgID := bson.NewObjectID()
var submitReq *chainv1.SubmitTransferRequest
executor := &gatewayProviderSettlementExecutor{
gatewayInvokeResolver: &fakeGatewayInvokeResolver{
client: &chainclient.Fake{
SubmitTransferFn: func(_ context.Context, req *chainv1.SubmitTransferRequest) (*chainv1.SubmitTransferResponse, error) {
submitReq = req
return &chainv1.SubmitTransferResponse{
Transfer: &chainv1.Transfer{
TransferRef: "trf-comment",
OperationRef: "op-comment",
},
}, nil
},
},
},
gatewayRegistry: &fakeGatewayRegistry{
items: []*model.GatewayInstanceDescriptor{
{
ID: "payment_gateway_settlement",
InstanceID: "payment_gateway_settlement",
Rail: discovery.RailProviderSettlement,
InvokeURI: "grpc://tgsettle-gateway",
IsEnabled: true,
},
},
},
}
out, err := executor.ExecuteProviderSettlement(context.Background(), sexec.StepRequest{
Payment: &agg.Payment{
OrganizationBoundBase: pm.OrganizationBoundBase{OrganizationRef: orgID},
PaymentRef: "payment-comment",
IdempotencyKey: "idem-comment",
IntentSnapshot: model.PaymentIntent{
Ref: "intent-comment",
Comment: "settlement-note-1",
Source: model.PaymentEndpoint{
Type: model.EndpointTypeManagedWallet,
ManagedWallet: &model.ManagedWalletEndpoint{
ManagedWalletRef: "wallet-src",
},
},
Amount: &paymenttypes.Money{Amount: "1.000000", Currency: "USDT"},
},
QuoteSnapshot: &model.PaymentQuoteSnapshot{
DebitAmount: &paymenttypes.Money{Amount: "1.000000", Currency: "USDT"},
ExpectedSettlementAmount: &paymenttypes.Money{Amount: "76.63", Currency: "RUB"},
},
},
Step: xplan.Step{
StepRef: "hop_2_settlement_fx_convert",
StepCode: "hop.2.settlement.fx_convert",
Action: discovery.RailOperationFXConvert,
Rail: discovery.RailProviderSettlement,
Gateway: "payment_gateway_settlement",
InstanceID: "payment_gateway_settlement",
HopRole: paymenttypes.QuoteRouteHopRoleDestination,
},
StepExecution: agg.StepExecution{
StepRef: "hop_2_settlement_fx_convert",
StepCode: "hop.2.settlement.fx_convert",
Attempt: 1,
},
})
if err != nil {
t.Fatalf("ExecuteProviderSettlement returned error: %v", err)
}
if submitReq == nil {
t.Fatal("expected transfer submission request")
}
if out == nil {
t.Fatal("expected output")
}
if got, want := out.StepExecution.Comment, "settlement-note-1"; got != want {
t.Fatalf("step comment mismatch: got=%q want=%q", got, want)
}
}
func TestGatewayProviderSettlementExecutor_ExecuteProviderSettlement_MissingSettlementAmount(t *testing.T) {
orgID := bson.NewObjectID()

View File

@@ -58,7 +58,7 @@ require (
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect
go.uber.org/multierr v1.11.0 // indirect
go.yaml.in/yaml/v2 v2.4.4 // indirect
golang.org/x/crypto v0.48.0 // indirect
golang.org/x/crypto v0.49.0 // indirect
golang.org/x/net v0.51.0 // indirect
golang.org/x/sync v0.20.0 // indirect
golang.org/x/sys v0.42.0 // indirect

View File

@@ -175,8 +175,8 @@ go.yaml.in/yaml/v2 v2.4.4 h1:tuyd0P+2Ont/d6e2rl3be67goVK4R6deVxCUX5vyPaQ=
go.yaml.in/yaml/v2 v2.4.4/go.mod h1:gMZqIpDtDqOfM0uNfy0SkpRhvUryYH0Z6wdMYcacYXQ=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=
golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos=
golang.org/x/crypto v0.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4=
golang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=

View File

@@ -32,6 +32,7 @@ func modelIntentFromQuoteIntent(src *transfer_intent_hydrator.QuoteIntent) model
Source: modelEndpointFromQuoteEndpoint(src.Source),
Destination: modelEndpointFromQuoteEndpoint(src.Destination),
Amount: cloneModelMoney(src.Amount),
Comment: strings.TrimSpace(src.Comment),
FX: fxIntentFromHydratedIntent(src),
RequiresFX: src.RequiresFX,
Attributes: cloneStringMap(src.Attributes),

View File

@@ -149,9 +149,6 @@ func (h *TransferIntentHydrator) HydrateOne(ctx context.Context, in HydrateOneIn
"initiator_ref": strings.TrimSpace(in.InitiatorRef),
},
}
if intent.Comment != "" {
intent.Attributes["comment"] = intent.Comment
}
return intent, nil
}

View File

@@ -99,6 +99,12 @@ func TestHydrateOne_SuccessWithInlineMethods(t *testing.T) {
if got.Amount == nil || got.Amount.Amount != "10.25" {
t.Fatalf("unexpected amount: %#v", got.Amount)
}
if got.Comment != "transfer note" {
t.Fatalf("unexpected comment: got=%q", got.Comment)
}
if _, ok := got.Attributes["comment"]; ok {
t.Fatalf("comment must not be persisted in attributes")
}
}
func TestHydrateOne_PropagatesFXIntent(t *testing.T) {

View File

@@ -35,7 +35,7 @@ require (
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect
go.uber.org/multierr v1.11.0 // indirect
go.yaml.in/yaml/v2 v2.4.4 // indirect
golang.org/x/crypto v0.48.0 // indirect
golang.org/x/crypto v0.49.0 // indirect
golang.org/x/sync v0.20.0 // indirect
golang.org/x/sys v0.42.0 // indirect
golang.org/x/text v0.35.0 // indirect

View File

@@ -158,8 +158,8 @@ go.yaml.in/yaml/v2 v2.4.4 h1:tuyd0P+2Ont/d6e2rl3be67goVK4R6deVxCUX5vyPaQ=
go.yaml.in/yaml/v2 v2.4.4/go.mod h1:gMZqIpDtDqOfM0uNfy0SkpRhvUryYH0Z6wdMYcacYXQ=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=
golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos=
golang.org/x/crypto v0.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4=
golang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=

View File

@@ -214,6 +214,7 @@ type PaymentIntent struct {
Source PaymentEndpoint `bson:"source" json:"source"`
Destination PaymentEndpoint `bson:"destination" json:"destination"`
Amount *paymenttypes.Money `bson:"amount" json:"amount"`
Comment string `bson:"comment,omitempty" json:"comment,omitempty"`
RequiresFX bool `bson:"requiresFx,omitempty" json:"requiresFx,omitempty"`
FX *FXIntent `bson:"fx,omitempty" json:"fx,omitempty"`
FeePolicy *paymenttypes.FeePolicy `bson:"feePolicy,omitempty" json:"feePolicy,omitempty"`
@@ -399,6 +400,7 @@ func (p *Payment) Normalize() {
p.Intent.Attributes[k] = strings.TrimSpace(v)
}
}
p.Intent.Comment = strings.TrimSpace(p.Intent.Comment)
p.Intent.FeeTreatment = FeeTreatment(strings.TrimSpace(string(p.Intent.FeeTreatment)))
p.Intent.SettlementCurrency = strings.TrimSpace(p.Intent.SettlementCurrency)
if p.Intent.Customer != nil {