fixed operations idempotency

This commit is contained in:
Stephan D
2026-03-04 02:27:12 +01:00
parent f06208348b
commit 8377b6b2af
16 changed files with 353 additions and 80 deletions

View File

@@ -110,6 +110,7 @@ func mapCallbackToState(clock clockpkg.Clock, cfg monetix.Config, cb monetixCall
ProviderCode: cb.Operation.Code,
ProviderMessage: cb.Operation.Message,
ProviderPaymentId: fallbackProviderPaymentID(cb),
OperationRef: strings.TrimSpace(cb.Payment.ID),
UpdatedAt: now,
CreatedAt: now,
}

View File

@@ -21,6 +21,7 @@ func (s *Service) handleCreateCardPayout(ctx context.Context, req *mntxv1.CardPa
log := s.logger.Named("card_payout")
log.Info("Create card payout request received",
zap.String("payout_id", strings.TrimSpace(req.GetPayoutId())),
zap.String("operation_ref", strings.TrimSpace(req.GetOperationRef())),
zap.String("customer_id", strings.TrimSpace(req.GetCustomerId())),
zap.Int64("amount_minor", req.GetAmountMinor()),
zap.String("currency", strings.ToUpper(strings.TrimSpace(req.GetCurrency()))),
@@ -47,6 +48,7 @@ func (s *Service) handleCreateCardTokenPayout(ctx context.Context, req *mntxv1.C
log := s.logger.Named("card_token_payout")
log.Info("Create card token payout request received",
zap.String("payout_id", strings.TrimSpace(req.GetPayoutId())),
zap.String("operation_ref", strings.TrimSpace(req.GetOperationRef())),
zap.String("customer_id", strings.TrimSpace(req.GetCustomerId())),
zap.Int64("amount_minor", req.GetAmountMinor()),
zap.String("currency", strings.ToUpper(strings.TrimSpace(req.GetCurrency()))),
@@ -133,6 +135,9 @@ func sanitizeCardPayoutRequest(req *mntxv1.CardPayoutRequest) *mntxv1.CardPayout
r.Currency = strings.ToUpper(strings.TrimSpace(r.GetCurrency()))
r.CardPan = strings.TrimSpace(r.GetCardPan())
r.CardHolder = strings.TrimSpace(r.GetCardHolder())
r.OperationRef = strings.TrimSpace(r.GetOperationRef())
r.IdempotencyKey = strings.TrimSpace(r.GetIdempotencyKey())
r.IntentRef = strings.TrimSpace(r.GetIntentRef())
return r
}
@@ -160,6 +165,9 @@ func sanitizeCardTokenPayoutRequest(req *mntxv1.CardTokenPayoutRequest) *mntxv1.
r.CardToken = strings.TrimSpace(r.GetCardToken())
r.CardHolder = strings.TrimSpace(r.GetCardHolder())
r.MaskedPan = strings.TrimSpace(r.GetMaskedPan())
r.OperationRef = strings.TrimSpace(r.GetOperationRef())
r.IdempotencyKey = strings.TrimSpace(r.GetIdempotencyKey())
r.IntentRef = strings.TrimSpace(r.GetIntentRef())
return r
}
@@ -206,7 +214,7 @@ func buildCardPayoutRequest(projectID int64, req *mntxv1.CardPayoutRequest) mone
return monetix.CardPayoutRequest{
General: monetix.General{
ProjectID: projectID,
PaymentID: req.GetPayoutId(),
PaymentID: findOperationRef(req.GetOperationRef(), req.GetPayoutId()),
},
Customer: monetix.Customer{
ID: req.GetCustomerId(),
@@ -232,7 +240,7 @@ func buildCardTokenPayoutRequest(projectID int64, req *mntxv1.CardTokenPayoutReq
return monetix.CardTokenPayoutRequest{
General: monetix.General{
ProjectID: projectID,
PaymentID: req.GetPayoutId(),
PaymentID: findOperationRef(req.GetOperationRef(), req.GetPayoutId()),
},
Customer: monetix.Customer{
ID: req.GetCustomerId(),

View File

@@ -27,6 +27,16 @@ type cardPayoutStore struct {
data map[string]*model.CardPayout
}
func payoutStoreKey(state *model.CardPayout) string {
if state == nil {
return ""
}
if ref := state.OperationRef; ref != "" {
return ref
}
return state.PaymentRef
}
func newCardPayoutStore() *cardPayoutStore {
return &cardPayoutStore{
data: make(map[string]*model.CardPayout),
@@ -42,26 +52,43 @@ func (s *cardPayoutStore) FindByIdempotencyKey(_ context.Context, key string) (*
return nil, nil
}
func (s *cardPayoutStore) FindByPaymentID(_ context.Context, id string) (*model.CardPayout, error) {
v, ok := s.data[id]
if !ok {
return nil, nil
func (s *cardPayoutStore) FindByOperationRef(_ context.Context, ref string) (*model.CardPayout, error) {
for _, v := range s.data {
if v.OperationRef == ref {
return v, nil
}
}
return v, nil
return nil, nil
}
func (s *cardPayoutStore) FindByPaymentID(_ context.Context, id string) (*model.CardPayout, error) {
for _, v := range s.data {
if v.PaymentRef == id {
return v, nil
}
}
return nil, nil
}
func (s *cardPayoutStore) Upsert(_ context.Context, record *model.CardPayout) error {
s.data[record.PaymentRef] = record
s.data[payoutStoreKey(record)] = record
return nil
}
// Save is a helper for tests to pre-populate data.
func (s *cardPayoutStore) Save(state *model.CardPayout) {
s.data[state.PaymentRef] = state
s.data[payoutStoreKey(state)] = state
}
// Get is a helper for tests to retrieve data.
func (s *cardPayoutStore) Get(id string) (*model.CardPayout, bool) {
v, ok := s.data[id]
return v, ok
if v, ok := s.data[id]; ok {
return v, true
}
for _, v := range s.data {
if v.PaymentRef == id || v.OperationRef == id {
return v, true
}
}
return nil, false
}

View File

@@ -14,8 +14,8 @@ func validateCardPayoutRequest(req *mntxv1.CardPayoutRequest, cfg monetix.Config
return newPayoutError("invalid_request", merrors.InvalidArgument("request cannot be empty"))
}
if strings.TrimSpace(req.GetPayoutId()) == "" {
return newPayoutError("missing_payout_id", merrors.InvalidArgument("payout_id is required", "payout_id"))
if err := validateOperationIdentity(strings.TrimSpace(req.GetPayoutId()), strings.TrimSpace(req.GetOperationRef())); err != nil {
return err
}
if strings.TrimSpace(req.GetCustomerId()) == "" {
return newPayoutError("missing_customer_id", merrors.InvalidArgument("customer_id is required", "customer_id"))
@@ -81,3 +81,16 @@ func validateCardExpiryFields(month uint32, year uint32) error {
}
return nil
}
func validateOperationIdentity(payoutID, operationRef string) error {
payoutID = strings.TrimSpace(payoutID)
operationRef = strings.TrimSpace(operationRef)
switch {
case payoutID == "" && operationRef == "":
return newPayoutError("missing_operation_ref", merrors.InvalidArgument("operation_ref or payout_id is required", "operation_ref"))
case payoutID != "" && operationRef != "":
return newPayoutError("ambiguous_operation_ref", merrors.InvalidArgument("provide either operation_ref or payout_id, not both"))
default:
return nil
}
}

View File

@@ -24,9 +24,17 @@ func TestValidateCardPayoutRequest_Errors(t *testing.T) {
expected string
}{
{
name: "missing_payout_id",
name: "missing_operation_identity",
mutate: func(r *mntxv1.CardPayoutRequest) { r.PayoutId = "" },
expected: "missing_payout_id",
expected: "missing_operation_ref",
},
{
name: "both_operation_and_payout_identity",
mutate: func(r *mntxv1.CardPayoutRequest) {
r.PayoutId = "parent-1"
r.OperationRef = "parent-1:hop_1_card_payout_send"
},
expected: "ambiguous_operation_ref",
},
{
name: "missing_customer_id",

View File

@@ -39,6 +39,8 @@ type cardPayoutProcessor struct {
perTxMinAmountMinorByCurrency map[string]int64
}
const parentPaymentRefMetadataKey = "parent_payment_ref"
func mergePayoutStateWithExisting(state, existing *model.CardPayout) {
if state == nil || existing == nil {
return
@@ -57,13 +59,102 @@ func mergePayoutStateWithExisting(state, existing *model.CardPayout) {
if state.IntentRef == "" {
state.IntentRef = existing.IntentRef
}
if state.PaymentRef == "" {
state.PaymentRef = existing.PaymentRef
}
}
func findOperationRef(operationRef, payoutID string) string {
ref := strings.TrimSpace(operationRef)
if ref != "" {
return ref
}
return strings.TrimSpace(payoutID)
}
func parentPaymentRefFromOperationRef(operationRef string) string {
ref := strings.TrimSpace(operationRef)
if ref == "" {
return ""
}
if idx := strings.Index(ref, ":hop_"); idx > 0 {
return ref[:idx]
}
if idx := strings.Index(ref, ":"); idx > 0 {
return ref[:idx]
}
return ""
}
func parentPaymentRefFromMetadata(metadata map[string]string) string {
if len(metadata) == 0 {
return ""
}
return strings.TrimSpace(metadata[parentPaymentRefMetadataKey])
}
func resolveParentPaymentRef(payoutID, operationRef string, metadata map[string]string) string {
if parent := parentPaymentRefFromMetadata(metadata); parent != "" {
return parent
}
payoutRef := strings.TrimSpace(payoutID)
opRef := strings.TrimSpace(operationRef)
// Legacy callers may pass parent payment ref via payout_id and a distinct operation_ref.
if payoutRef != "" && (opRef == "" || payoutRef != opRef) {
return payoutRef
}
if parent := parentPaymentRefFromOperationRef(opRef); parent != "" {
return parent
}
if payoutRef != "" {
return payoutRef
}
return opRef
}
func (p *cardPayoutProcessor) findExistingPayoutState(ctx context.Context, state *model.CardPayout) (*model.CardPayout, error) {
if p == nil || state == nil {
return nil, nil
}
if opRef := strings.TrimSpace(state.OperationRef); opRef != "" {
existing, err := p.store.Payouts().FindByOperationRef(ctx, opRef)
if err == nil {
if existing != nil {
return existing, nil
}
}
if !errors.Is(err, merrors.ErrNoData) {
if err != nil {
return nil, err
}
}
// Legacy mode may still map operation ref to payout/payment ref.
if paymentRef := strings.TrimSpace(state.PaymentRef); paymentRef == "" || paymentRef != opRef {
return nil, nil
}
}
if paymentRef := strings.TrimSpace(state.PaymentRef); paymentRef != "" {
existing, err := p.store.Payouts().FindByPaymentID(ctx, paymentRef)
if err == nil {
if existing != nil {
return existing, nil
}
}
if !errors.Is(err, merrors.ErrNoData) {
if err != nil {
return nil, err
}
}
}
return nil, nil
}
func (p *cardPayoutProcessor) findAndMergePayoutState(ctx context.Context, state *model.CardPayout) (*model.CardPayout, error) {
if p == nil || state == nil {
return nil, nil
}
existing, err := p.store.Payouts().FindByPaymentID(ctx, state.PaymentRef)
existing, err := p.findExistingPayoutState(ctx, state)
if err != nil {
return nil, err
}
@@ -218,13 +309,15 @@ func (p *cardPayoutProcessor) Submit(ctx context.Context, req *mntxv1.CardPayout
}
req = sanitizeCardPayoutRequest(req)
operationRef := findOperationRef(req.GetOperationRef(), req.GetPayoutId())
parentPaymentRef := resolveParentPaymentRef(req.GetPayoutId(), operationRef, req.GetMetadata())
p.logger.Info("Submitting card payout",
zap.String("payout_id", strings.TrimSpace(req.GetPayoutId())),
zap.String("parent_payment_ref", parentPaymentRef),
zap.String("customer_id", strings.TrimSpace(req.GetCustomerId())),
zap.Int64("amount_minor", req.GetAmountMinor()),
zap.String("currency", strings.ToUpper(strings.TrimSpace(req.GetCurrency()))),
zap.String("operation_ref", strings.TrimSpace(req.GetOperationRef())),
zap.String("operation_ref", operationRef),
zap.String("idempotency_key", strings.TrimSpace(req.GetIdempotencyKey())),
)
@@ -235,7 +328,8 @@ func (p *cardPayoutProcessor) Submit(ctx context.Context, req *mntxv1.CardPayout
if err := validateCardPayoutRequest(req, p.config); err != nil {
p.logger.Warn("Card payout validation failed",
zap.String("payout_id", req.GetPayoutId()),
zap.String("parent_payment_ref", parentPaymentRef),
zap.String("operation_ref", operationRef),
zap.String("customer_id", req.GetCustomerId()),
zap.Error(err),
)
@@ -243,7 +337,8 @@ func (p *cardPayoutProcessor) Submit(ctx context.Context, req *mntxv1.CardPayout
}
if err := p.validatePerTxMinimum(req.GetAmountMinor(), req.GetCurrency()); err != nil {
p.logger.Warn("Card payout amount below configured minimum",
zap.String("payout_id", req.GetPayoutId()),
zap.String("parent_payment_ref", parentPaymentRef),
zap.String("operation_ref", operationRef),
zap.String("customer_id", req.GetCustomerId()),
zap.Int64("amount_minor", req.GetAmountMinor()),
zap.String("currency", strings.ToUpper(strings.TrimSpace(req.GetCurrency()))),
@@ -253,7 +348,7 @@ func (p *cardPayoutProcessor) Submit(ctx context.Context, req *mntxv1.CardPayout
return nil, err
}
projectID, err := p.resolveProjectID(req.GetProjectId(), "payout_id", req.GetPayoutId())
projectID, err := p.resolveProjectID(req.GetProjectId(), "operation_ref", operationRef)
if err != nil {
return nil, err
}
@@ -264,8 +359,8 @@ func (p *cardPayoutProcessor) Submit(ctx context.Context, req *mntxv1.CardPayout
Base: storable.Base{
ID: bson.NilObjectID,
},
PaymentRef: strings.TrimSpace(req.GetPayoutId()),
OperationRef: strings.TrimSpace(req.GetOperationRef()),
PaymentRef: parentPaymentRef,
OperationRef: operationRef,
IdempotencyKey: strings.TrimSpace(req.GetIdempotencyKey()),
IntentRef: strings.TrimSpace(req.GetIntentRef()),
ProjectID: projectID,
@@ -339,13 +434,15 @@ func (p *cardPayoutProcessor) SubmitToken(ctx context.Context, req *mntxv1.CardT
}
req = sanitizeCardTokenPayoutRequest(req)
operationRef := findOperationRef(req.GetOperationRef(), req.GetPayoutId())
parentPaymentRef := resolveParentPaymentRef(req.GetPayoutId(), operationRef, req.GetMetadata())
p.logger.Info("Submitting card token payout",
zap.String("payout_id", strings.TrimSpace(req.GetPayoutId())),
zap.String("parent_payment_ref", parentPaymentRef),
zap.String("customer_id", strings.TrimSpace(req.GetCustomerId())),
zap.Int64("amount_minor", req.GetAmountMinor()),
zap.String("currency", strings.ToUpper(strings.TrimSpace(req.GetCurrency()))),
zap.String("operation_ref", strings.TrimSpace(req.GetOperationRef())),
zap.String("operation_ref", operationRef),
zap.String("idempotency_key", strings.TrimSpace(req.GetIdempotencyKey())),
)
@@ -356,7 +453,8 @@ func (p *cardPayoutProcessor) SubmitToken(ctx context.Context, req *mntxv1.CardT
if err := validateCardTokenPayoutRequest(req, p.config); err != nil {
p.logger.Warn("Card token payout validation failed",
zap.String("payout_id", req.GetPayoutId()),
zap.String("parent_payment_ref", parentPaymentRef),
zap.String("operation_ref", operationRef),
zap.String("customer_id", req.GetCustomerId()),
zap.Error(err),
)
@@ -364,7 +462,8 @@ func (p *cardPayoutProcessor) SubmitToken(ctx context.Context, req *mntxv1.CardT
}
if err := p.validatePerTxMinimum(req.GetAmountMinor(), req.GetCurrency()); err != nil {
p.logger.Warn("Card token payout amount below configured minimum",
zap.String("payout_id", req.GetPayoutId()),
zap.String("parent_payment_ref", parentPaymentRef),
zap.String("operation_ref", operationRef),
zap.String("customer_id", req.GetCustomerId()),
zap.Int64("amount_minor", req.GetAmountMinor()),
zap.String("currency", strings.ToUpper(strings.TrimSpace(req.GetCurrency()))),
@@ -374,16 +473,17 @@ func (p *cardPayoutProcessor) SubmitToken(ctx context.Context, req *mntxv1.CardT
return nil, err
}
projectID, err := p.resolveProjectID(req.GetProjectId(), "payout_id", req.GetPayoutId())
projectID, err := p.resolveProjectID(req.GetProjectId(), "operation_ref", operationRef)
if err != nil {
return nil, err
}
now := p.clock.Now()
state := &model.CardPayout{
PaymentRef: strings.TrimSpace(req.GetPayoutId()),
OperationRef: strings.TrimSpace(req.GetOperationRef()),
PaymentRef: parentPaymentRef,
OperationRef: operationRef,
IdempotencyKey: strings.TrimSpace(req.GetIdempotencyKey()),
IntentRef: strings.TrimSpace(req.GetIntentRef()),
ProjectID: projectID,
CustomerID: strings.TrimSpace(req.GetCustomerId()),
AmountMinor: req.GetAmountMinor(),
@@ -509,21 +609,29 @@ func (p *cardPayoutProcessor) Status(ctx context.Context, payoutID string) (*mnt
}
id := strings.TrimSpace(payoutID)
p.logger.Info("Card payout status requested", zap.String("payout_id", id))
p.logger.Info("Card payout status requested", zap.String("operation_or_payout_id", id))
if id == "" {
p.logger.Warn("Payout status requested with empty payout_id")
return nil, merrors.InvalidArgument("payout_id is required", "payout_id")
}
state, err := p.store.Payouts().FindByPaymentID(ctx, id)
if err != nil || state == nil {
p.logger.Warn("Payout status not found", zap.String("payout_id", id), zap.Error(err))
return nil, merrors.NoData("payout not found")
state, err := p.store.Payouts().FindByOperationRef(ctx, id)
if err != nil && !errors.Is(err, merrors.ErrNoData) {
p.logger.Warn("Payout status lookup by operation ref failed", zap.String("operation_ref", id), zap.Error(err))
return nil, err
}
if state == nil || errors.Is(err, merrors.ErrNoData) {
state, err = p.store.Payouts().FindByPaymentID(ctx, id)
if err != nil || state == nil {
p.logger.Warn("Payout status not found", zap.String("operation_or_payout_id", id), zap.Error(err))
return nil, merrors.NoData("payout not found")
}
}
p.logger.Info("Card payout status resolved",
zap.String("payment_ref", state.PaymentRef),
zap.String("operation_ref", state.OperationRef),
zap.String("status", string(state.Status)),
)

View File

@@ -13,8 +13,8 @@ func validateCardTokenPayoutRequest(req *mntxv1.CardTokenPayoutRequest, cfg mone
return newPayoutError("invalid_request", merrors.InvalidArgument("request cannot be empty"))
}
if strings.TrimSpace(req.GetPayoutId()) == "" {
return newPayoutError("missing_payout_id", merrors.InvalidArgument("payout_id is required", "payout_id"))
if err := validateOperationIdentity(strings.TrimSpace(req.GetPayoutId()), strings.TrimSpace(req.GetOperationRef())); err != nil {
return err
}
if strings.TrimSpace(req.GetCustomerId()) == "" {
return newPayoutError("missing_customer_id", merrors.InvalidArgument("customer_id is required", "customer_id"))

View File

@@ -24,9 +24,17 @@ func TestValidateCardTokenPayoutRequest_Errors(t *testing.T) {
expected string
}{
{
name: "missing_payout_id",
name: "missing_operation_identity",
mutate: func(r *mntxv1.CardTokenPayoutRequest) { r.PayoutId = "" },
expected: "missing_payout_id",
expected: "missing_operation_ref",
},
{
name: "both_operation_and_payout_identity",
mutate: func(r *mntxv1.CardTokenPayoutRequest) {
r.PayoutId = "parent-1"
r.OperationRef = "parent-1:hop_1_card_payout_send"
},
expected: "ambiguous_operation_ref",
},
{
name: "missing_customer_id",
@@ -63,7 +71,7 @@ func TestValidateCardTokenPayoutRequest_Errors(t *testing.T) {
expected: "missing_card_token",
},
{
name: "missing_customer_city_when_required",
name: "missing_customer_city_when_required",
mutate: func(r *mntxv1.CardTokenPayoutRequest) {
r.CustomerCountry = "US"
r.CustomerCity = ""

View File

@@ -71,12 +71,10 @@ func (s *Service) SubmitOperation(ctx context.Context, req *connectorv1.SubmitOp
}
payoutID := strings.TrimSpace(reader.String("payout_id"))
if payoutID == "" {
payoutID = strings.TrimSpace(op.GetIdempotencyKey())
}
payoutID = operationIDForRequest(payoutID, operationRef, idempotencyKey)
if strings.TrimSpace(reader.String("card_token")) != "" {
resp, err := s.CreateCardTokenPayout(ctx, buildCardTokenPayoutRequestFromParams(reader, payoutID, amountMinor, currency))
resp, err := s.CreateCardTokenPayout(ctx, buildCardTokenPayoutRequestFromParams(reader, payoutID, idempotencyKey, operationRef, intentRef, amountMinor, currency))
if err != nil {
return &connectorv1.SubmitOperationResponse{Receipt: &connectorv1.OperationReceipt{Error: connectorError(mapErrorCode(err), err.Error(), op, "")}}, nil
}
@@ -169,7 +167,38 @@ func currencyFromOperation(op *connectorv1.Operation) string {
return strings.ToUpper(currency)
}
func buildCardTokenPayoutRequestFromParams(reader params.Reader, payoutID string, amountMinor int64, currency string) *mntxv1.CardTokenPayoutRequest {
func operationIDForRequest(payoutID, operationRef, idempotencyKey string) string {
if ref := strings.TrimSpace(operationRef); ref != "" {
return ref
}
if ref := strings.TrimSpace(payoutID); ref != "" {
return ref
}
return strings.TrimSpace(idempotencyKey)
}
func metadataFromReader(reader params.Reader) map[string]string {
metadata := reader.StringMap("metadata")
if metadata == nil {
metadata = map[string]string{}
}
if parentRef := strings.TrimSpace(reader.String("payout_id")); parentRef != "" && strings.TrimSpace(metadata[parentPaymentRefMetadataKey]) == "" {
metadata[parentPaymentRefMetadataKey] = parentRef
}
if len(metadata) == 0 {
return nil
}
return metadata
}
func buildCardTokenPayoutRequestFromParams(reader params.Reader,
payoutID, idempotencyKey, operationRef, intentRef string,
amountMinor int64, currency string) *mntxv1.CardTokenPayoutRequest {
operationRef = strings.TrimSpace(operationRef)
payoutID = strings.TrimSpace(payoutID)
if operationRef != "" {
payoutID = ""
}
req := &mntxv1.CardTokenPayoutRequest{
PayoutId: payoutID,
ProjectId: readerInt64(reader, "project_id"),
@@ -188,7 +217,10 @@ func buildCardTokenPayoutRequestFromParams(reader params.Reader, payoutID string
CardToken: strings.TrimSpace(reader.String("card_token")),
CardHolder: strings.TrimSpace(reader.String("card_holder")),
MaskedPan: strings.TrimSpace(reader.String("masked_pan")),
Metadata: reader.StringMap("metadata"),
Metadata: metadataFromReader(reader),
OperationRef: operationRef,
IdempotencyKey: strings.TrimSpace(idempotencyKey),
IntentRef: strings.TrimSpace(intentRef),
}
return req
}
@@ -196,6 +228,11 @@ func buildCardTokenPayoutRequestFromParams(reader params.Reader, payoutID string
func buildCardPayoutRequestFromParams(reader params.Reader,
payoutID, idempotencyKey, operationRef, intentRef string,
amountMinor int64, currency string) *mntxv1.CardPayoutRequest {
operationRef = strings.TrimSpace(operationRef)
payoutID = strings.TrimSpace(payoutID)
if operationRef != "" {
payoutID = ""
}
return &mntxv1.CardPayoutRequest{
PayoutId: payoutID,
ProjectId: readerInt64(reader, "project_id"),
@@ -215,10 +252,10 @@ func buildCardPayoutRequestFromParams(reader params.Reader,
CardExpYear: uint32(readerInt64(reader, "card_exp_year")),
CardExpMonth: uint32(readerInt64(reader, "card_exp_month")),
CardHolder: strings.TrimSpace(reader.String("card_holder")),
Metadata: reader.StringMap("metadata"),
Metadata: metadataFromReader(reader),
OperationRef: operationRef,
IdempotencyKey: idempotencyKey,
IntentRef: intentRef,
IdempotencyKey: strings.TrimSpace(idempotencyKey),
IntentRef: strings.TrimSpace(intentRef),
}
}
@@ -236,7 +273,7 @@ func payoutReceipt(state *mntxv1.CardPayoutState) *connectorv1.OperationReceipt
}
}
return &connectorv1.OperationReceipt{
OperationId: strings.TrimSpace(state.GetPayoutId()),
OperationId: firstNonEmpty(strings.TrimSpace(state.GetOperationRef()), strings.TrimSpace(state.GetPayoutId())),
Status: payoutStatusToOperation(state.GetStatus()),
ProviderRef: strings.TrimSpace(state.GetProviderPaymentId()),
}
@@ -247,7 +284,7 @@ func payoutToOperation(state *mntxv1.CardPayoutState) *connectorv1.Operation {
return nil
}
return &connectorv1.Operation{
OperationId: strings.TrimSpace(state.GetPayoutId()),
OperationId: firstNonEmpty(strings.TrimSpace(state.GetOperationRef()), strings.TrimSpace(state.GetPayoutId())),
Type: connectorv1.OperationType_PAYOUT,
Status: payoutStatusToOperation(state.GetStatus()),
Money: &moneyv1.Money{

View File

@@ -41,7 +41,7 @@ func CardPayoutStateFromProto(clock clockpkg.Clock, p *mntxv1.CardPayoutState) *
func StateToProto(m *model.CardPayout) *mntxv1.CardPayoutState {
return &mntxv1.CardPayoutState{
PayoutId: m.PaymentRef,
PayoutId: firstNonEmpty(m.OperationRef, m.PaymentRef),
ProjectId: m.ProjectID,
CustomerId: m.CustomerID,
AmountMinor: m.AmountMinor,