fixed operations idempotency
This commit is contained in:
@@ -39,6 +39,8 @@ type gatewayClient struct {
|
||||
logger mlogger.Logger
|
||||
}
|
||||
|
||||
const parentPaymentRefMetadataKey = "parent_payment_ref"
|
||||
|
||||
// New dials the Monetix gateway.
|
||||
func New(ctx context.Context, cfg Config, opts ...grpc.DialOption) (Client, error) {
|
||||
cfg.setDefaults()
|
||||
@@ -104,7 +106,7 @@ func (g *gatewayClient) CreateCardPayout(ctx context.Context, req *mntxv1.CardPa
|
||||
if resp.GetReceipt() != nil && resp.GetReceipt().GetError() != nil {
|
||||
return nil, connectorError(resp.GetReceipt().GetError())
|
||||
}
|
||||
return &mntxv1.CardPayoutResponse{Payout: payoutFromReceipt(req.GetPayoutId(), resp.GetReceipt())}, nil
|
||||
return &mntxv1.CardPayoutResponse{Payout: payoutFromReceipt(req.GetPayoutId(), req.GetOperationRef(), resp.GetReceipt())}, nil
|
||||
}
|
||||
|
||||
func (g *gatewayClient) CreateCardTokenPayout(ctx context.Context, req *mntxv1.CardTokenPayoutRequest) (*mntxv1.CardTokenPayoutResponse, error) {
|
||||
@@ -121,7 +123,7 @@ func (g *gatewayClient) CreateCardTokenPayout(ctx context.Context, req *mntxv1.C
|
||||
if resp.GetReceipt() != nil && resp.GetReceipt().GetError() != nil {
|
||||
return nil, connectorError(resp.GetReceipt().GetError())
|
||||
}
|
||||
return &mntxv1.CardTokenPayoutResponse{Payout: payoutFromReceipt(req.GetPayoutId(), resp.GetReceipt())}, nil
|
||||
return &mntxv1.CardTokenPayoutResponse{Payout: payoutFromReceipt(req.GetPayoutId(), req.GetOperationRef(), resp.GetReceipt())}, nil
|
||||
}
|
||||
|
||||
func (g *gatewayClient) GetCardPayoutStatus(ctx context.Context, req *mntxv1.GetCardPayoutStatusRequest) (*mntxv1.GetCardPayoutStatusResponse, error) {
|
||||
@@ -147,10 +149,12 @@ func operationFromCardPayout(req *mntxv1.CardPayoutRequest) (*connectorv1.Operat
|
||||
}
|
||||
params := payoutParamsFromCard(req)
|
||||
money := moneyFromMinor(req.GetAmountMinor(), req.GetCurrency())
|
||||
operationRef := fallbackNonEmpty(req.GetOperationRef(), req.GetPayoutId())
|
||||
idempotencyKey := fallbackNonEmpty(req.GetIdempotencyKey(), operationRef)
|
||||
op := &connectorv1.Operation{
|
||||
Type: connectorv1.OperationType_PAYOUT,
|
||||
IdempotencyKey: strings.TrimSpace(req.GetIdempotencyKey()),
|
||||
OperationRef: strings.TrimSpace(req.GetOperationRef()),
|
||||
IdempotencyKey: idempotencyKey,
|
||||
OperationRef: operationRef,
|
||||
IntentRef: strings.TrimSpace(req.GetIntentRef()),
|
||||
Money: money,
|
||||
Params: structFromMap(params),
|
||||
@@ -165,9 +169,13 @@ func operationFromTokenPayout(req *mntxv1.CardTokenPayoutRequest) (*connectorv1.
|
||||
}
|
||||
params := payoutParamsFromToken(req)
|
||||
money := moneyFromMinor(req.GetAmountMinor(), req.GetCurrency())
|
||||
operationRef := fallbackNonEmpty(req.GetOperationRef(), req.GetPayoutId())
|
||||
idempotencyKey := fallbackNonEmpty(req.GetIdempotencyKey(), operationRef)
|
||||
op := &connectorv1.Operation{
|
||||
Type: connectorv1.OperationType_PAYOUT,
|
||||
IdempotencyKey: strings.TrimSpace(req.GetPayoutId()),
|
||||
IdempotencyKey: idempotencyKey,
|
||||
OperationRef: operationRef,
|
||||
IntentRef: strings.TrimSpace(req.GetIntentRef()),
|
||||
Money: money,
|
||||
Params: structFromMap(params),
|
||||
}
|
||||
@@ -192,8 +200,8 @@ func setOperationRolesFromMetadata(op *connectorv1.Operation, metadata map[strin
|
||||
}
|
||||
|
||||
func payoutParamsFromCard(req *mntxv1.CardPayoutRequest) map[string]interface{} {
|
||||
metadata := metadataWithParentPaymentRef(req.GetMetadata(), req.GetPayoutId())
|
||||
params := map[string]interface{}{
|
||||
"payout_id": strings.TrimSpace(req.GetPayoutId()),
|
||||
"project_id": req.GetProjectId(),
|
||||
"customer_id": strings.TrimSpace(req.GetCustomerId()),
|
||||
"customer_first_name": strings.TrimSpace(req.GetCustomerFirstName()),
|
||||
@@ -212,15 +220,15 @@ func payoutParamsFromCard(req *mntxv1.CardPayoutRequest) map[string]interface{}
|
||||
"card_exp_month": req.GetCardExpMonth(),
|
||||
"card_holder": strings.TrimSpace(req.GetCardHolder()),
|
||||
}
|
||||
if len(req.GetMetadata()) > 0 {
|
||||
params["metadata"] = mapStringToInterface(req.GetMetadata())
|
||||
if len(metadata) > 0 {
|
||||
params["metadata"] = mapStringToInterface(metadata)
|
||||
}
|
||||
return params
|
||||
}
|
||||
|
||||
func payoutParamsFromToken(req *mntxv1.CardTokenPayoutRequest) map[string]interface{} {
|
||||
metadata := metadataWithParentPaymentRef(req.GetMetadata(), req.GetPayoutId())
|
||||
params := map[string]interface{}{
|
||||
"payout_id": strings.TrimSpace(req.GetPayoutId()),
|
||||
"project_id": req.GetProjectId(),
|
||||
"customer_id": strings.TrimSpace(req.GetCustomerId()),
|
||||
"customer_first_name": strings.TrimSpace(req.GetCustomerFirstName()),
|
||||
@@ -238,8 +246,8 @@ func payoutParamsFromToken(req *mntxv1.CardTokenPayoutRequest) map[string]interf
|
||||
"card_holder": strings.TrimSpace(req.GetCardHolder()),
|
||||
"masked_pan": strings.TrimSpace(req.GetMaskedPan()),
|
||||
}
|
||||
if len(req.GetMetadata()) > 0 {
|
||||
params["metadata"] = mapStringToInterface(req.GetMetadata())
|
||||
if len(metadata) > 0 {
|
||||
params["metadata"] = mapStringToInterface(metadata)
|
||||
}
|
||||
return params
|
||||
}
|
||||
@@ -255,16 +263,53 @@ func moneyFromMinor(amount int64, currency string) *moneyv1.Money {
|
||||
}
|
||||
}
|
||||
|
||||
func payoutFromReceipt(payoutID string, receipt *connectorv1.OperationReceipt) *mntxv1.CardPayoutState {
|
||||
state := &mntxv1.CardPayoutState{PayoutId: strings.TrimSpace(payoutID)}
|
||||
func payoutFromReceipt(payoutID, operationRef string, receipt *connectorv1.OperationReceipt) *mntxv1.CardPayoutState {
|
||||
state := &mntxv1.CardPayoutState{
|
||||
PayoutId: fallbackNonEmpty(operationRef, payoutID),
|
||||
}
|
||||
if receipt == nil {
|
||||
return state
|
||||
}
|
||||
if opID := strings.TrimSpace(receipt.GetOperationId()); opID != "" {
|
||||
state.PayoutId = opID
|
||||
}
|
||||
state.Status = payoutStatusFromOperation(receipt.GetStatus())
|
||||
state.ProviderPaymentId = strings.TrimSpace(receipt.GetProviderRef())
|
||||
return state
|
||||
}
|
||||
|
||||
func fallbackNonEmpty(values ...string) string {
|
||||
for _, value := range values {
|
||||
clean := strings.TrimSpace(value)
|
||||
if clean != "" {
|
||||
return clean
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func metadataWithParentPaymentRef(source map[string]string, parentPaymentRef string) map[string]string {
|
||||
parentPaymentRef = strings.TrimSpace(parentPaymentRef)
|
||||
if len(source) == 0 && parentPaymentRef == "" {
|
||||
return nil
|
||||
}
|
||||
out := map[string]string{}
|
||||
for key, value := range source {
|
||||
k := strings.TrimSpace(key)
|
||||
if k == "" {
|
||||
continue
|
||||
}
|
||||
out[k] = strings.TrimSpace(value)
|
||||
}
|
||||
if parentPaymentRef != "" && strings.TrimSpace(out[parentPaymentRefMetadataKey]) == "" {
|
||||
out[parentPaymentRefMetadataKey] = parentPaymentRef
|
||||
}
|
||||
if len(out) == 0 {
|
||||
return nil
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func payoutFromOperation(op *connectorv1.Operation) *mntxv1.CardPayoutState {
|
||||
if op == nil {
|
||||
return nil
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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 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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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)
|
||||
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("payout_id", id), zap.Error(err))
|
||||
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)),
|
||||
)
|
||||
|
||||
|
||||
@@ -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"))
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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{
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -19,6 +19,7 @@ const (
|
||||
payoutsCollection = "card_payouts"
|
||||
payoutIdemField = "idempotencyKey"
|
||||
payoutIdField = "paymentRef"
|
||||
payoutOpField = "operationRef"
|
||||
)
|
||||
|
||||
type Payouts struct {
|
||||
@@ -36,6 +37,16 @@ func NewPayouts(logger mlogger.Logger, db *mongo.Database) (*Payouts, error) {
|
||||
logger = logger.Named("payouts").With(zap.String("collection", payoutsCollection))
|
||||
|
||||
repo := repository.CreateMongoRepository(db, payoutsCollection)
|
||||
if err := repo.CreateIndex(&ri.Definition{
|
||||
Keys: []ri.Key{{Field: payoutOpField, Sort: ri.Asc}},
|
||||
Unique: true,
|
||||
Sparse: true,
|
||||
}); err != nil {
|
||||
logger.Error("Failed to create payouts operation index",
|
||||
zap.Error(err),
|
||||
zap.String("index_field", payoutOpField))
|
||||
return nil, err
|
||||
}
|
||||
if err := repo.CreateIndex(&ri.Definition{
|
||||
Keys: []ri.Key{{Field: payoutIdemField, Sort: ri.Asc}},
|
||||
Unique: true,
|
||||
@@ -63,6 +74,10 @@ func (p *Payouts) FindByIdempotencyKey(ctx context.Context, key string) (*model.
|
||||
return p.findOneByField(ctx, payoutIdemField, key)
|
||||
}
|
||||
|
||||
func (p *Payouts) FindByOperationRef(ctx context.Context, operationRef string) (*model.CardPayout, error) {
|
||||
return p.findOneByField(ctx, payoutOpField, operationRef)
|
||||
}
|
||||
|
||||
func (p *Payouts) FindByPaymentID(ctx context.Context, paymentID string) (*model.CardPayout, error) {
|
||||
return p.findOneByField(ctx, payoutIdField, paymentID)
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ type Repository interface {
|
||||
|
||||
type PayoutsStore interface {
|
||||
FindByIdempotencyKey(ctx context.Context, key string) (*model.CardPayout, error)
|
||||
FindByOperationRef(ctx context.Context, key string) (*model.CardPayout, error)
|
||||
FindByPaymentID(ctx context.Context, key string) (*model.CardPayout, error)
|
||||
Upsert(ctx context.Context, record *model.CardPayout) error
|
||||
}
|
||||
|
||||
@@ -74,7 +74,6 @@ func (e *gatewayCardPayoutExecutor) ExecuteCardPayout(ctx context.Context, req s
|
||||
|
||||
stepToken := cardPayoutStepToken(req.Step)
|
||||
operationRef := cardPayoutOperationRef(req.Payment, stepToken)
|
||||
payoutRef := cardPayoutRef(req.Payment)
|
||||
idempotencyKey := cardPayoutIdempotencyKey(req.Payment, stepToken)
|
||||
projectID := cardPayoutProjectID(req.Payment)
|
||||
customer := cardPayoutCustomerFromPayment(req.Payment, card, req.Step.Metadata)
|
||||
@@ -85,7 +84,6 @@ func (e *gatewayCardPayoutExecutor) ExecuteCardPayout(ctx context.Context, req s
|
||||
var responsePayout *mntxv1.CardPayoutState
|
||||
if token := strings.TrimSpace(card.Token); token != "" {
|
||||
resp, createErr := client.CreateCardTokenPayout(ctx, &mntxv1.CardTokenPayoutRequest{
|
||||
PayoutId: payoutRef,
|
||||
ProjectId: projectID,
|
||||
CustomerId: customer.id,
|
||||
CustomerFirstName: customer.firstName,
|
||||
@@ -123,7 +121,6 @@ func (e *gatewayCardPayoutExecutor) ExecuteCardPayout(ctx context.Context, req s
|
||||
return nil, merrors.InvalidArgument("card payout send: card expiry is required")
|
||||
}
|
||||
resp, createErr := client.CreateCardPayout(ctx, &mntxv1.CardPayoutRequest{
|
||||
PayoutId: payoutRef,
|
||||
ProjectId: projectID,
|
||||
CustomerId: customer.id,
|
||||
CustomerFirstName: customer.firstName,
|
||||
@@ -155,8 +152,8 @@ func (e *gatewayCardPayoutExecutor) ExecuteCardPayout(ctx context.Context, req s
|
||||
responsePayout = resp.GetPayout()
|
||||
}
|
||||
|
||||
resolvedPayoutRef := firstNonEmpty(strings.TrimSpace(responsePayout.GetPayoutId()), payoutRef)
|
||||
resolvedOperationRef := firstNonEmpty(strings.TrimSpace(responsePayout.GetOperationRef()), operationRef)
|
||||
resolvedPayoutRef := firstNonEmpty(strings.TrimSpace(responsePayout.GetPayoutId()), resolvedOperationRef)
|
||||
gatewayInstanceID := firstNonEmpty(
|
||||
strings.TrimSpace(req.Step.InstanceID),
|
||||
strings.TrimSpace(gateway.InstanceID),
|
||||
@@ -356,14 +353,6 @@ func cardPayoutOperationRef(payment *agg.Payment, stepToken string) string {
|
||||
return joinRef(base, stepToken)
|
||||
}
|
||||
|
||||
func cardPayoutRef(payment *agg.Payment) string {
|
||||
base := ""
|
||||
if payment != nil {
|
||||
base = firstNonEmpty(strings.TrimSpace(payment.PaymentRef), strings.TrimSpace(payment.IdempotencyKey), "card_payout")
|
||||
}
|
||||
return base
|
||||
}
|
||||
|
||||
func cardPayoutIdempotencyKey(payment *agg.Payment, stepToken string) string {
|
||||
base := ""
|
||||
if payment != nil {
|
||||
@@ -549,6 +538,9 @@ func cardPayoutMetadata(payment *agg.Payment, step xplan.Step) map[string]string
|
||||
out = map[string]string{}
|
||||
}
|
||||
if payment != nil {
|
||||
if parentPaymentRef := strings.TrimSpace(payment.PaymentRef); parentPaymentRef != "" {
|
||||
out[settlementMetadataParentPaymentRef] = parentPaymentRef
|
||||
}
|
||||
if quoteRef := firstNonEmpty(
|
||||
strings.TrimSpace(payment.QuotationRef),
|
||||
strings.TrimSpace(quoteRefFromSnapshot(payment.QuoteSnapshot)),
|
||||
|
||||
@@ -122,7 +122,7 @@ func TestGatewayCardPayoutExecutor_ExecuteCardPayout_SubmitsCardPayout(t *testin
|
||||
if got, want := dialAddress, "mntx-gateway:50051"; got != want {
|
||||
t.Fatalf("dial address mismatch: got=%q want=%q", got, want)
|
||||
}
|
||||
if got, want := payoutReq.GetPayoutId(), "payment-1"; got != want {
|
||||
if got, want := payoutReq.GetPayoutId(), ""; 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 {
|
||||
@@ -143,6 +143,9 @@ 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.GetMetadata()[settlementMetadataParentPaymentRef], "payment-1"; got != want {
|
||||
t.Fatalf("parent_payment_ref metadata mismatch: got=%q want=%q", got, want)
|
||||
}
|
||||
if len(out.StepExecution.ExternalRefs) != 3 {
|
||||
t.Fatalf("expected 3 external refs, got=%d", len(out.StepExecution.ExternalRefs))
|
||||
}
|
||||
@@ -263,12 +266,18 @@ func TestGatewayCardPayoutExecutor_ExecuteCardPayout_UsesStepMetadataOverrides(t
|
||||
if got, want := payoutReq.GetCurrency(), "RUB"; got != want {
|
||||
t.Fatalf("currency mismatch: got=%q want=%q", got, want)
|
||||
}
|
||||
if got, want := payoutReq.GetPayoutId(), ""; got != want {
|
||||
t.Fatalf("payout_id mismatch: got=%q want=%q", got, want)
|
||||
}
|
||||
if got, want := payoutReq.GetCardPan(), "2200700142860162"; got != want {
|
||||
t.Fatalf("card pan mismatch: got=%q want=%q", got, want)
|
||||
}
|
||||
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.GetMetadata()[settlementMetadataParentPaymentRef], "payment-2"; got != want {
|
||||
t.Fatalf("parent_payment_ref metadata mismatch: got=%q want=%q", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGatewayCardPayoutExecutor_ExecuteCardPayout_RequiresGatewayRegistry(t *testing.T) {
|
||||
|
||||
@@ -17,6 +17,7 @@ import (
|
||||
const (
|
||||
settlementMetadataQuoteRef = "quote_ref"
|
||||
settlementMetadataOutgoingLeg = "outgoing_leg"
|
||||
settlementMetadataParentPaymentRef = "parent_payment_ref"
|
||||
)
|
||||
|
||||
type gatewayProviderSettlementExecutor struct {
|
||||
|
||||
Reference in New Issue
Block a user