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

@@ -202,6 +202,7 @@ func payoutParamsFromCard(req *mntxv1.CardPayoutRequest) map[string]interface{}
params := map[string]interface{}{
"project_id": req.GetProjectId(),
"parent_payment_ref": strings.TrimSpace(req.GetParentPaymentRef()),
"comment": strings.TrimSpace(req.GetComment()),
"customer_id": strings.TrimSpace(req.GetCustomerId()),
"customer_first_name": strings.TrimSpace(req.GetCustomerFirstName()),
"customer_middle_name": strings.TrimSpace(req.GetCustomerMiddleName()),
@@ -230,6 +231,7 @@ func payoutParamsFromToken(req *mntxv1.CardTokenPayoutRequest) map[string]interf
params := map[string]interface{}{
"project_id": req.GetProjectId(),
"parent_payment_ref": strings.TrimSpace(req.GetParentPaymentRef()),
"comment": strings.TrimSpace(req.GetComment()),
"customer_id": strings.TrimSpace(req.GetCustomerId()),
"customer_first_name": strings.TrimSpace(req.GetCustomerFirstName()),
"customer_middle_name": strings.TrimSpace(req.GetCustomerMiddleName()),
@@ -299,6 +301,9 @@ func sanitizeMetadata(source map[string]string) map[string]string {
if k == "" {
continue
}
if strings.EqualFold(k, "comment") {
continue
}
out[k] = strings.TrimSpace(value)
}
if len(out) == 0 {
@@ -320,6 +325,11 @@ func payoutFromOperation(op *connectorv1.Operation) *mntxv1.CardPayoutState {
state.Currency = strings.TrimSpace(money.GetCurrency())
state.AmountMinor = minorFromMoney(money)
}
if params := op.GetParams(); params != nil {
if field, ok := params.GetFields()["comment"]; ok {
state.Comment = strings.TrimSpace(field.GetStringValue())
}
}
return state
}

View File

@@ -142,3 +142,49 @@ func TestCreateCardTokenPayout_UsesOperationRefWhenReceiptOperationIDMissing(t *
t.Fatalf("payout_id mismatch: got=%q want=%q", got, want)
}
}
func TestCreateCardPayout_SendsCommentAsSeparateParam(t *testing.T) {
stub := &stubConnectorClient{
submitResp: &connectorv1.SubmitOperationResponse{
Receipt: &connectorv1.OperationReceipt{
OperationId: "payment-4:hop_4_card_payout_send",
Status: connectorv1.OperationStatus_OPERATION_WAITING,
},
},
}
client := &gatewayClient{client: stub, cfg: Config{}}
_, err := client.CreateCardPayout(context.Background(), &mntxv1.CardPayoutRequest{
OperationRef: "payment-4:hop_4_card_payout_send",
IdempotencyKey: "idem-4",
AmountMinor: 1000,
Currency: "RUB",
Comment: "invoice-44",
Metadata: map[string]string{
"comment": "legacy-comment",
"trace_id": "trace-44",
},
})
if err != nil {
t.Fatalf("CreateCardPayout returned error: %v", err)
}
if stub.submitReq == nil || stub.submitReq.GetOperation() == nil {
t.Fatal("expected submitted operation")
}
fields := stub.submitReq.GetOperation().GetParams().GetFields()
if got, want := fields["comment"].GetStringValue(), "invoice-44"; got != want {
t.Fatalf("comment param mismatch: got=%q want=%q", got, want)
}
metadataField, ok := fields["metadata"]
if !ok || metadataField == nil || metadataField.GetStructValue() == nil {
t.Fatal("expected metadata param")
}
metadata := metadataField.GetStructValue().GetFields()
if _, hasComment := metadata["comment"]; hasComment {
t.Fatalf("metadata must not include comment key")
}
if got, want := metadata["trace_id"].GetStringValue(), "trace-44"; got != want {
t.Fatalf("trace_id metadata mismatch: got=%q want=%q", got, want)
}
}

View File

@@ -46,7 +46,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

@@ -141,6 +141,7 @@ func sanitizeCardPayoutRequest(req *mntxv1.CardPayoutRequest) *mntxv1.CardPayout
r.OperationRef = strings.TrimSpace(r.GetOperationRef())
r.IdempotencyKey = strings.TrimSpace(r.GetIdempotencyKey())
r.IntentRef = strings.TrimSpace(r.GetIntentRef())
r.Comment = strings.TrimSpace(r.GetComment())
return r
}
@@ -172,6 +173,7 @@ func sanitizeCardTokenPayoutRequest(req *mntxv1.CardTokenPayoutRequest) *mntxv1.
r.OperationRef = strings.TrimSpace(r.GetOperationRef())
r.IdempotencyKey = strings.TrimSpace(r.GetIdempotencyKey())
r.IntentRef = strings.TrimSpace(r.GetIntentRef())
r.Comment = strings.TrimSpace(r.GetComment())
return r
}

View File

@@ -87,6 +87,9 @@ func mergePayoutStateWithExisting(state, existing *model.CardPayout) {
if state.IntentRef == "" {
state.IntentRef = existing.IntentRef
}
if state.Comment == "" {
state.Comment = existing.Comment
}
if existing.PaymentRef != "" {
state.PaymentRef = existing.PaymentRef
}
@@ -168,6 +171,7 @@ func payoutStateLogFields(state *model.CardPayout) []zap.Field {
zap.String("operation_ref", state.OperationRef),
zap.String("idempotency_key", state.IdempotencyKey),
zap.String("intent_ref", state.IntentRef),
zap.String("comment", state.Comment),
}
}
@@ -1070,6 +1074,7 @@ func (p *cardPayoutProcessor) Submit(ctx context.Context, req *mntxv1.CardPayout
OperationRef: operationRef,
IdempotencyKey: strings.TrimSpace(req.GetIdempotencyKey()),
IntentRef: strings.TrimSpace(req.GetIntentRef()),
Comment: strings.TrimSpace(req.GetComment()),
ProjectID: projectID,
CustomerID: strings.TrimSpace(req.GetCustomerId()),
AmountMinor: req.GetAmountMinor(),
@@ -1237,6 +1242,7 @@ func (p *cardPayoutProcessor) SubmitToken(ctx context.Context, req *mntxv1.CardT
OperationRef: operationRef,
IdempotencyKey: strings.TrimSpace(req.GetIdempotencyKey()),
IntentRef: strings.TrimSpace(req.GetIntentRef()),
Comment: strings.TrimSpace(req.GetComment()),
ProjectID: projectID,
CustomerID: strings.TrimSpace(req.GetCustomerId()),
AmountMinor: req.GetAmountMinor(),

View File

@@ -80,6 +80,7 @@ func TestCardPayoutProcessor_Submit_Success(t *testing.T) {
req := validCardPayoutRequest()
req.ProjectId = 0
req.Comment = "invoice-1001"
resp, err := processor.Submit(context.Background(), req)
if err != nil {
@@ -105,6 +106,12 @@ func TestCardPayoutProcessor_Submit_Success(t *testing.T) {
if stored.ProviderPaymentID == "" {
t.Fatalf("expected provider payment id")
}
if got, want := stored.Comment, "invoice-1001"; got != want {
t.Fatalf("expected stored comment %q, got %q", want, got)
}
if got, want := resp.GetPayout().GetComment(), "invoice-1001"; got != want {
t.Fatalf("expected response comment %q, got %q", want, got)
}
if !stored.CreatedAt.Equal(existingCreated) {
t.Fatalf("expected created_at preserved in model, got %v", stored.CreatedAt)
}

View File

@@ -129,6 +129,7 @@ func mntxOperationParams() []*connectorv1.OperationParamSpec {
{Key: "amount_minor", Type: connectorv1.ParamType_INT, Required: false},
{Key: "project_id", Type: connectorv1.ParamType_INT, Required: false},
{Key: "parent_payment_ref", Type: connectorv1.ParamType_STRING, Required: true},
{Key: "comment", Type: connectorv1.ParamType_STRING, Required: false},
{Key: "customer_middle_name", Type: connectorv1.ParamType_STRING, Required: false},
{Key: "customer_country", Type: connectorv1.ParamType_STRING, Required: false},
{Key: "customer_state", Type: connectorv1.ParamType_STRING, Required: false},
@@ -188,7 +189,18 @@ func metadataFromReader(reader params.Reader) map[string]string {
if len(metadata) == 0 {
return nil
}
return metadata
out := make(map[string]string, len(metadata))
for key, value := range metadata {
k := strings.TrimSpace(key)
if k == "" || strings.EqualFold(k, "comment") {
continue
}
out[k] = strings.TrimSpace(value)
}
if len(out) == 0 {
return nil
}
return out
}
func buildCardTokenPayoutRequestFromParams(reader params.Reader,
@@ -218,6 +230,7 @@ func buildCardTokenPayoutRequestFromParams(reader params.Reader,
CardToken: strings.TrimSpace(reader.String("card_token")),
CardHolder: strings.TrimSpace(reader.String("card_holder")),
MaskedPan: strings.TrimSpace(reader.String("masked_pan")),
Comment: strings.TrimSpace(reader.String("comment")),
Metadata: metadataFromReader(reader),
OperationRef: operationRef,
IdempotencyKey: strings.TrimSpace(idempotencyKey),
@@ -254,6 +267,7 @@ func buildCardPayoutRequestFromParams(reader params.Reader,
CardExpYear: uint32(readerInt64(reader, "card_exp_year")), //nolint:gosec // values are validated by request validators
CardExpMonth: uint32(readerInt64(reader, "card_exp_month")), //nolint:gosec // values are validated by request validators
CardHolder: strings.TrimSpace(reader.String("card_holder")),
Comment: strings.TrimSpace(reader.String("comment")),
Metadata: metadataFromReader(reader),
OperationRef: operationRef,
IdempotencyKey: strings.TrimSpace(idempotencyKey),
@@ -305,6 +319,9 @@ func payoutToOperation(state *mntxv1.CardPayoutState) *connectorv1.Operation {
params["payment_ref"] = paymentRef
params["parent_payment_ref"] = paymentRef
}
if comment := strings.TrimSpace(state.GetComment()); comment != "" {
params["comment"] = comment
}
if providerCode := strings.TrimSpace(state.GetProviderCode()); providerCode != "" {
params["provider_code"] = providerCode
}

View File

@@ -26,6 +26,7 @@ func CardPayoutStateFromProto(clock clockpkg.Clock, p *mntxv1.CardPayoutState) *
PaymentRef: strings.TrimSpace(p.GetParentPaymentRef()),
OperationRef: p.GetOperationRef(),
IntentRef: p.GetIntentRef(),
Comment: strings.TrimSpace(p.GetComment()),
IdempotencyKey: p.GetIdempotencyKey(),
ProjectID: p.ProjectId,
CustomerID: p.CustomerId,
@@ -52,6 +53,7 @@ func StateToProto(m *model.CardPayout) *mntxv1.CardPayoutState {
ProviderCode: m.ProviderCode,
ProviderMessage: m.ProviderMessage,
ProviderPaymentId: m.ProviderPaymentID,
Comment: strings.TrimSpace(m.Comment),
CreatedAt: timestamppb.New(m.CreatedAt),
UpdatedAt: timestamppb.New(m.UpdatedAt),
}

View File

@@ -13,6 +13,7 @@ type CardPayout struct {
OperationRef string `bson:"operationRef" json:"operation_ref"`
IdempotencyKey string `bson:"idempotencyKey" json:"idempotency_key"`
IntentRef string `bson:"intentRef" json:"intentRef"`
Comment string `bson:"comment,omitempty" json:"comment,omitempty"`
ProjectID int64 `bson:"projectId" json:"project_id"`
CustomerID string `bson:"customerId" json:"customer_id"`
AmountMinor int64 `bson:"amountMinor" json:"amount_minor"`

View File

@@ -89,6 +89,7 @@ func (p *Payouts) Upsert(ctx context.Context, record *model.CardPayout) error {
record.OperationRef = strings.TrimSpace(record.OperationRef)
record.PaymentRef = strings.TrimSpace(record.PaymentRef)
record.CustomerID = strings.TrimSpace(record.CustomerID)
record.Comment = strings.TrimSpace(record.Comment)
record.ProviderCode = strings.TrimSpace(record.ProviderCode)
record.ProviderPaymentID = strings.TrimSpace(record.ProviderPaymentID)