mntx-624 #625
@@ -20,12 +20,15 @@ This service now supports Monetix “payout by card”.
|
|||||||
Payload is built per Monetix spec:
|
Payload is built per Monetix spec:
|
||||||
```
|
```
|
||||||
{
|
{
|
||||||
"general": { "project_id": <int>, "payment_id": "<payout_id>", "signature": "<hmac>" },
|
"general": { "project_id": <int>, "payment_id": "<operation_ref>", "signature": "<hmac>" },
|
||||||
"customer": { id, first_name, last_name, middle_name?, ip_address, zip?, country?, state?, city?, address? },
|
"customer": { id, first_name, last_name, middle_name?, ip_address, zip?, country?, state?, city?, address? },
|
||||||
"payment": { amount: <minor_units>, currency: "<ISO-4217>" },
|
"payment": { amount: <minor_units>, currency: "<ISO-4217>" },
|
||||||
"card": { pan, year?, month?, card_holder }
|
"card": { pan, year?, month?, card_holder }
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
Gateway request contract additionally requires `parent_payment_ref` as a first-class field
|
||||||
|
(separate from Monetix `payment_id`).
|
||||||
|
|
||||||
Signature: HMAC-SHA256 over the JSON body (without `signature`), using `MONETIX_SECRET_KEY`.
|
Signature: HMAC-SHA256 over the JSON body (without `signature`), using `MONETIX_SECRET_KEY`.
|
||||||
|
|
||||||
## Callback handling
|
## Callback handling
|
||||||
|
|||||||
@@ -104,7 +104,7 @@ func (g *gatewayClient) CreateCardPayout(ctx context.Context, req *mntxv1.CardPa
|
|||||||
if resp.GetReceipt() != nil && resp.GetReceipt().GetError() != nil {
|
if resp.GetReceipt() != nil && resp.GetReceipt().GetError() != nil {
|
||||||
return nil, connectorError(resp.GetReceipt().GetError())
|
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(), req.GetParentPaymentRef(), resp.GetReceipt())}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (g *gatewayClient) CreateCardTokenPayout(ctx context.Context, req *mntxv1.CardTokenPayoutRequest) (*mntxv1.CardTokenPayoutResponse, error) {
|
func (g *gatewayClient) CreateCardTokenPayout(ctx context.Context, req *mntxv1.CardTokenPayoutRequest) (*mntxv1.CardTokenPayoutResponse, error) {
|
||||||
@@ -121,7 +121,7 @@ func (g *gatewayClient) CreateCardTokenPayout(ctx context.Context, req *mntxv1.C
|
|||||||
if resp.GetReceipt() != nil && resp.GetReceipt().GetError() != nil {
|
if resp.GetReceipt() != nil && resp.GetReceipt().GetError() != nil {
|
||||||
return nil, connectorError(resp.GetReceipt().GetError())
|
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(), req.GetParentPaymentRef(), resp.GetReceipt())}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (g *gatewayClient) GetCardPayoutStatus(ctx context.Context, req *mntxv1.GetCardPayoutStatusRequest) (*mntxv1.GetCardPayoutStatusResponse, error) {
|
func (g *gatewayClient) GetCardPayoutStatus(ctx context.Context, req *mntxv1.GetCardPayoutStatusRequest) (*mntxv1.GetCardPayoutStatusResponse, error) {
|
||||||
@@ -147,10 +147,12 @@ func operationFromCardPayout(req *mntxv1.CardPayoutRequest) (*connectorv1.Operat
|
|||||||
}
|
}
|
||||||
params := payoutParamsFromCard(req)
|
params := payoutParamsFromCard(req)
|
||||||
money := moneyFromMinor(req.GetAmountMinor(), req.GetCurrency())
|
money := moneyFromMinor(req.GetAmountMinor(), req.GetCurrency())
|
||||||
|
operationRef := fallbackNonEmpty(req.GetOperationRef(), req.GetPayoutId())
|
||||||
|
idempotencyKey := fallbackNonEmpty(req.GetIdempotencyKey(), operationRef)
|
||||||
op := &connectorv1.Operation{
|
op := &connectorv1.Operation{
|
||||||
Type: connectorv1.OperationType_PAYOUT,
|
Type: connectorv1.OperationType_PAYOUT,
|
||||||
IdempotencyKey: strings.TrimSpace(req.GetIdempotencyKey()),
|
IdempotencyKey: idempotencyKey,
|
||||||
OperationRef: strings.TrimSpace(req.GetOperationRef()),
|
OperationRef: operationRef,
|
||||||
IntentRef: strings.TrimSpace(req.GetIntentRef()),
|
IntentRef: strings.TrimSpace(req.GetIntentRef()),
|
||||||
Money: money,
|
Money: money,
|
||||||
Params: structFromMap(params),
|
Params: structFromMap(params),
|
||||||
@@ -165,9 +167,13 @@ func operationFromTokenPayout(req *mntxv1.CardTokenPayoutRequest) (*connectorv1.
|
|||||||
}
|
}
|
||||||
params := payoutParamsFromToken(req)
|
params := payoutParamsFromToken(req)
|
||||||
money := moneyFromMinor(req.GetAmountMinor(), req.GetCurrency())
|
money := moneyFromMinor(req.GetAmountMinor(), req.GetCurrency())
|
||||||
|
operationRef := fallbackNonEmpty(req.GetOperationRef(), req.GetPayoutId())
|
||||||
|
idempotencyKey := fallbackNonEmpty(req.GetIdempotencyKey(), operationRef)
|
||||||
op := &connectorv1.Operation{
|
op := &connectorv1.Operation{
|
||||||
Type: connectorv1.OperationType_PAYOUT,
|
Type: connectorv1.OperationType_PAYOUT,
|
||||||
IdempotencyKey: strings.TrimSpace(req.GetPayoutId()),
|
IdempotencyKey: idempotencyKey,
|
||||||
|
OperationRef: operationRef,
|
||||||
|
IntentRef: strings.TrimSpace(req.GetIntentRef()),
|
||||||
Money: money,
|
Money: money,
|
||||||
Params: structFromMap(params),
|
Params: structFromMap(params),
|
||||||
}
|
}
|
||||||
@@ -192,9 +198,10 @@ func setOperationRolesFromMetadata(op *connectorv1.Operation, metadata map[strin
|
|||||||
}
|
}
|
||||||
|
|
||||||
func payoutParamsFromCard(req *mntxv1.CardPayoutRequest) map[string]interface{} {
|
func payoutParamsFromCard(req *mntxv1.CardPayoutRequest) map[string]interface{} {
|
||||||
|
metadata := sanitizeMetadata(req.GetMetadata())
|
||||||
params := map[string]interface{}{
|
params := map[string]interface{}{
|
||||||
"payout_id": strings.TrimSpace(req.GetPayoutId()),
|
|
||||||
"project_id": req.GetProjectId(),
|
"project_id": req.GetProjectId(),
|
||||||
|
"parent_payment_ref": strings.TrimSpace(req.GetParentPaymentRef()),
|
||||||
"customer_id": strings.TrimSpace(req.GetCustomerId()),
|
"customer_id": strings.TrimSpace(req.GetCustomerId()),
|
||||||
"customer_first_name": strings.TrimSpace(req.GetCustomerFirstName()),
|
"customer_first_name": strings.TrimSpace(req.GetCustomerFirstName()),
|
||||||
"customer_middle_name": strings.TrimSpace(req.GetCustomerMiddleName()),
|
"customer_middle_name": strings.TrimSpace(req.GetCustomerMiddleName()),
|
||||||
@@ -212,16 +219,17 @@ func payoutParamsFromCard(req *mntxv1.CardPayoutRequest) map[string]interface{}
|
|||||||
"card_exp_month": req.GetCardExpMonth(),
|
"card_exp_month": req.GetCardExpMonth(),
|
||||||
"card_holder": strings.TrimSpace(req.GetCardHolder()),
|
"card_holder": strings.TrimSpace(req.GetCardHolder()),
|
||||||
}
|
}
|
||||||
if len(req.GetMetadata()) > 0 {
|
if len(metadata) > 0 {
|
||||||
params["metadata"] = mapStringToInterface(req.GetMetadata())
|
params["metadata"] = mapStringToInterface(metadata)
|
||||||
}
|
}
|
||||||
return params
|
return params
|
||||||
}
|
}
|
||||||
|
|
||||||
func payoutParamsFromToken(req *mntxv1.CardTokenPayoutRequest) map[string]interface{} {
|
func payoutParamsFromToken(req *mntxv1.CardTokenPayoutRequest) map[string]interface{} {
|
||||||
|
metadata := sanitizeMetadata(req.GetMetadata())
|
||||||
params := map[string]interface{}{
|
params := map[string]interface{}{
|
||||||
"payout_id": strings.TrimSpace(req.GetPayoutId()),
|
|
||||||
"project_id": req.GetProjectId(),
|
"project_id": req.GetProjectId(),
|
||||||
|
"parent_payment_ref": strings.TrimSpace(req.GetParentPaymentRef()),
|
||||||
"customer_id": strings.TrimSpace(req.GetCustomerId()),
|
"customer_id": strings.TrimSpace(req.GetCustomerId()),
|
||||||
"customer_first_name": strings.TrimSpace(req.GetCustomerFirstName()),
|
"customer_first_name": strings.TrimSpace(req.GetCustomerFirstName()),
|
||||||
"customer_middle_name": strings.TrimSpace(req.GetCustomerMiddleName()),
|
"customer_middle_name": strings.TrimSpace(req.GetCustomerMiddleName()),
|
||||||
@@ -238,8 +246,8 @@ func payoutParamsFromToken(req *mntxv1.CardTokenPayoutRequest) map[string]interf
|
|||||||
"card_holder": strings.TrimSpace(req.GetCardHolder()),
|
"card_holder": strings.TrimSpace(req.GetCardHolder()),
|
||||||
"masked_pan": strings.TrimSpace(req.GetMaskedPan()),
|
"masked_pan": strings.TrimSpace(req.GetMaskedPan()),
|
||||||
}
|
}
|
||||||
if len(req.GetMetadata()) > 0 {
|
if len(metadata) > 0 {
|
||||||
params["metadata"] = mapStringToInterface(req.GetMetadata())
|
params["metadata"] = mapStringToInterface(metadata)
|
||||||
}
|
}
|
||||||
return params
|
return params
|
||||||
}
|
}
|
||||||
@@ -255,16 +263,50 @@ func moneyFromMinor(amount int64, currency string) *moneyv1.Money {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func payoutFromReceipt(payoutID string, receipt *connectorv1.OperationReceipt) *mntxv1.CardPayoutState {
|
func payoutFromReceipt(payoutID, operationRef, parentPaymentRef string, receipt *connectorv1.OperationReceipt) *mntxv1.CardPayoutState {
|
||||||
state := &mntxv1.CardPayoutState{PayoutId: strings.TrimSpace(payoutID)}
|
state := &mntxv1.CardPayoutState{
|
||||||
|
PayoutId: fallbackNonEmpty(operationRef, payoutID),
|
||||||
|
ParentPaymentRef: strings.TrimSpace(parentPaymentRef),
|
||||||
|
}
|
||||||
if receipt == nil {
|
if receipt == nil {
|
||||||
return state
|
return state
|
||||||
}
|
}
|
||||||
|
if opID := strings.TrimSpace(receipt.GetOperationId()); opID != "" {
|
||||||
|
state.PayoutId = opID
|
||||||
|
}
|
||||||
state.Status = payoutStatusFromOperation(receipt.GetStatus())
|
state.Status = payoutStatusFromOperation(receipt.GetStatus())
|
||||||
state.ProviderPaymentId = strings.TrimSpace(receipt.GetProviderRef())
|
state.ProviderPaymentId = strings.TrimSpace(receipt.GetProviderRef())
|
||||||
return state
|
return state
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func fallbackNonEmpty(values ...string) string {
|
||||||
|
for _, value := range values {
|
||||||
|
clean := strings.TrimSpace(value)
|
||||||
|
if clean != "" {
|
||||||
|
return clean
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func sanitizeMetadata(source map[string]string) map[string]string {
|
||||||
|
if len(source) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
out := map[string]string{}
|
||||||
|
for key, value := range source {
|
||||||
|
k := strings.TrimSpace(key)
|
||||||
|
if k == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
out[k] = strings.TrimSpace(value)
|
||||||
|
}
|
||||||
|
if len(out) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
func payoutFromOperation(op *connectorv1.Operation) *mntxv1.CardPayoutState {
|
func payoutFromOperation(op *connectorv1.Operation) *mntxv1.CardPayoutState {
|
||||||
if op == nil {
|
if op == nil {
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
@@ -110,6 +110,7 @@ func mapCallbackToState(clock clockpkg.Clock, cfg monetix.Config, cb monetixCall
|
|||||||
ProviderCode: cb.Operation.Code,
|
ProviderCode: cb.Operation.Code,
|
||||||
ProviderMessage: cb.Operation.Message,
|
ProviderMessage: cb.Operation.Message,
|
||||||
ProviderPaymentId: fallbackProviderPaymentID(cb),
|
ProviderPaymentId: fallbackProviderPaymentID(cb),
|
||||||
|
OperationRef: strings.TrimSpace(cb.Payment.ID),
|
||||||
UpdatedAt: now,
|
UpdatedAt: now,
|
||||||
CreatedAt: now,
|
CreatedAt: now,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,6 +21,8 @@ func (s *Service) handleCreateCardPayout(ctx context.Context, req *mntxv1.CardPa
|
|||||||
log := s.logger.Named("card_payout")
|
log := s.logger.Named("card_payout")
|
||||||
log.Info("Create card payout request received",
|
log.Info("Create card payout request received",
|
||||||
zap.String("payout_id", strings.TrimSpace(req.GetPayoutId())),
|
zap.String("payout_id", strings.TrimSpace(req.GetPayoutId())),
|
||||||
|
zap.String("operation_ref", strings.TrimSpace(req.GetOperationRef())),
|
||||||
|
zap.String("parent_payment_ref", strings.TrimSpace(req.GetParentPaymentRef())),
|
||||||
zap.String("customer_id", strings.TrimSpace(req.GetCustomerId())),
|
zap.String("customer_id", strings.TrimSpace(req.GetCustomerId())),
|
||||||
zap.Int64("amount_minor", req.GetAmountMinor()),
|
zap.Int64("amount_minor", req.GetAmountMinor()),
|
||||||
zap.String("currency", strings.ToUpper(strings.TrimSpace(req.GetCurrency()))),
|
zap.String("currency", strings.ToUpper(strings.TrimSpace(req.GetCurrency()))),
|
||||||
@@ -47,6 +49,8 @@ func (s *Service) handleCreateCardTokenPayout(ctx context.Context, req *mntxv1.C
|
|||||||
log := s.logger.Named("card_token_payout")
|
log := s.logger.Named("card_token_payout")
|
||||||
log.Info("Create card token payout request received",
|
log.Info("Create card token payout request received",
|
||||||
zap.String("payout_id", strings.TrimSpace(req.GetPayoutId())),
|
zap.String("payout_id", strings.TrimSpace(req.GetPayoutId())),
|
||||||
|
zap.String("operation_ref", strings.TrimSpace(req.GetOperationRef())),
|
||||||
|
zap.String("parent_payment_ref", strings.TrimSpace(req.GetParentPaymentRef())),
|
||||||
zap.String("customer_id", strings.TrimSpace(req.GetCustomerId())),
|
zap.String("customer_id", strings.TrimSpace(req.GetCustomerId())),
|
||||||
zap.Int64("amount_minor", req.GetAmountMinor()),
|
zap.Int64("amount_minor", req.GetAmountMinor()),
|
||||||
zap.String("currency", strings.ToUpper(strings.TrimSpace(req.GetCurrency()))),
|
zap.String("currency", strings.ToUpper(strings.TrimSpace(req.GetCurrency()))),
|
||||||
@@ -120,6 +124,7 @@ func sanitizeCardPayoutRequest(req *mntxv1.CardPayoutRequest) *mntxv1.CardPayout
|
|||||||
return req
|
return req
|
||||||
}
|
}
|
||||||
r.PayoutId = strings.TrimSpace(r.GetPayoutId())
|
r.PayoutId = strings.TrimSpace(r.GetPayoutId())
|
||||||
|
r.ParentPaymentRef = strings.TrimSpace(r.GetParentPaymentRef())
|
||||||
r.CustomerId = strings.TrimSpace(r.GetCustomerId())
|
r.CustomerId = strings.TrimSpace(r.GetCustomerId())
|
||||||
r.CustomerFirstName = strings.TrimSpace(r.GetCustomerFirstName())
|
r.CustomerFirstName = strings.TrimSpace(r.GetCustomerFirstName())
|
||||||
r.CustomerMiddleName = strings.TrimSpace(r.GetCustomerMiddleName())
|
r.CustomerMiddleName = strings.TrimSpace(r.GetCustomerMiddleName())
|
||||||
@@ -133,6 +138,9 @@ func sanitizeCardPayoutRequest(req *mntxv1.CardPayoutRequest) *mntxv1.CardPayout
|
|||||||
r.Currency = strings.ToUpper(strings.TrimSpace(r.GetCurrency()))
|
r.Currency = strings.ToUpper(strings.TrimSpace(r.GetCurrency()))
|
||||||
r.CardPan = strings.TrimSpace(r.GetCardPan())
|
r.CardPan = strings.TrimSpace(r.GetCardPan())
|
||||||
r.CardHolder = strings.TrimSpace(r.GetCardHolder())
|
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
|
return r
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -146,6 +154,7 @@ func sanitizeCardTokenPayoutRequest(req *mntxv1.CardTokenPayoutRequest) *mntxv1.
|
|||||||
return req
|
return req
|
||||||
}
|
}
|
||||||
r.PayoutId = strings.TrimSpace(r.GetPayoutId())
|
r.PayoutId = strings.TrimSpace(r.GetPayoutId())
|
||||||
|
r.ParentPaymentRef = strings.TrimSpace(r.GetParentPaymentRef())
|
||||||
r.CustomerId = strings.TrimSpace(r.GetCustomerId())
|
r.CustomerId = strings.TrimSpace(r.GetCustomerId())
|
||||||
r.CustomerFirstName = strings.TrimSpace(r.GetCustomerFirstName())
|
r.CustomerFirstName = strings.TrimSpace(r.GetCustomerFirstName())
|
||||||
r.CustomerMiddleName = strings.TrimSpace(r.GetCustomerMiddleName())
|
r.CustomerMiddleName = strings.TrimSpace(r.GetCustomerMiddleName())
|
||||||
@@ -160,6 +169,9 @@ func sanitizeCardTokenPayoutRequest(req *mntxv1.CardTokenPayoutRequest) *mntxv1.
|
|||||||
r.CardToken = strings.TrimSpace(r.GetCardToken())
|
r.CardToken = strings.TrimSpace(r.GetCardToken())
|
||||||
r.CardHolder = strings.TrimSpace(r.GetCardHolder())
|
r.CardHolder = strings.TrimSpace(r.GetCardHolder())
|
||||||
r.MaskedPan = strings.TrimSpace(r.GetMaskedPan())
|
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
|
return r
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -206,7 +218,7 @@ func buildCardPayoutRequest(projectID int64, req *mntxv1.CardPayoutRequest) mone
|
|||||||
return monetix.CardPayoutRequest{
|
return monetix.CardPayoutRequest{
|
||||||
General: monetix.General{
|
General: monetix.General{
|
||||||
ProjectID: projectID,
|
ProjectID: projectID,
|
||||||
PaymentID: req.GetPayoutId(),
|
PaymentID: findOperationRef(req.GetOperationRef(), req.GetPayoutId()),
|
||||||
},
|
},
|
||||||
Customer: monetix.Customer{
|
Customer: monetix.Customer{
|
||||||
ID: req.GetCustomerId(),
|
ID: req.GetCustomerId(),
|
||||||
@@ -232,7 +244,7 @@ func buildCardTokenPayoutRequest(projectID int64, req *mntxv1.CardTokenPayoutReq
|
|||||||
return monetix.CardTokenPayoutRequest{
|
return monetix.CardTokenPayoutRequest{
|
||||||
General: monetix.General{
|
General: monetix.General{
|
||||||
ProjectID: projectID,
|
ProjectID: projectID,
|
||||||
PaymentID: req.GetPayoutId(),
|
PaymentID: findOperationRef(req.GetOperationRef(), req.GetPayoutId()),
|
||||||
},
|
},
|
||||||
Customer: monetix.Customer{
|
Customer: monetix.Customer{
|
||||||
ID: req.GetCustomerId(),
|
ID: req.GetCustomerId(),
|
||||||
|
|||||||
@@ -27,6 +27,16 @@ type cardPayoutStore struct {
|
|||||||
data map[string]*model.CardPayout
|
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 {
|
func newCardPayoutStore() *cardPayoutStore {
|
||||||
return &cardPayoutStore{
|
return &cardPayoutStore{
|
||||||
data: make(map[string]*model.CardPayout),
|
data: make(map[string]*model.CardPayout),
|
||||||
@@ -42,26 +52,43 @@ func (s *cardPayoutStore) FindByIdempotencyKey(_ context.Context, key string) (*
|
|||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *cardPayoutStore) FindByPaymentID(_ context.Context, id string) (*model.CardPayout, error) {
|
func (s *cardPayoutStore) FindByOperationRef(_ context.Context, ref string) (*model.CardPayout, error) {
|
||||||
v, ok := s.data[id]
|
for _, v := range s.data {
|
||||||
if !ok {
|
if v.OperationRef == ref {
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
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 {
|
func (s *cardPayoutStore) Upsert(_ context.Context, record *model.CardPayout) error {
|
||||||
s.data[record.PaymentRef] = record
|
s.data[payoutStoreKey(record)] = record
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Save is a helper for tests to pre-populate data.
|
// Save is a helper for tests to pre-populate data.
|
||||||
func (s *cardPayoutStore) Save(state *model.CardPayout) {
|
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.
|
// Get is a helper for tests to retrieve data.
|
||||||
func (s *cardPayoutStore) Get(id string) (*model.CardPayout, bool) {
|
func (s *cardPayoutStore) Get(id string) (*model.CardPayout, bool) {
|
||||||
v, ok := s.data[id]
|
if v, ok := s.data[id]; ok {
|
||||||
return v, 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,11 @@ func validateCardPayoutRequest(req *mntxv1.CardPayoutRequest, cfg monetix.Config
|
|||||||
return newPayoutError("invalid_request", merrors.InvalidArgument("request cannot be empty"))
|
return newPayoutError("invalid_request", merrors.InvalidArgument("request cannot be empty"))
|
||||||
}
|
}
|
||||||
|
|
||||||
if strings.TrimSpace(req.GetPayoutId()) == "" {
|
if err := validateOperationIdentity(strings.TrimSpace(req.GetPayoutId()), strings.TrimSpace(req.GetOperationRef())); err != nil {
|
||||||
return newPayoutError("missing_payout_id", merrors.InvalidArgument("payout_id is required", "payout_id"))
|
return err
|
||||||
|
}
|
||||||
|
if strings.TrimSpace(req.GetParentPaymentRef()) == "" {
|
||||||
|
return newPayoutError("missing_parent_payment_ref", merrors.InvalidArgument("parent_payment_ref is required", "parent_payment_ref"))
|
||||||
}
|
}
|
||||||
if strings.TrimSpace(req.GetCustomerId()) == "" {
|
if strings.TrimSpace(req.GetCustomerId()) == "" {
|
||||||
return newPayoutError("missing_customer_id", merrors.InvalidArgument("customer_id is required", "customer_id"))
|
return newPayoutError("missing_customer_id", merrors.InvalidArgument("customer_id is required", "customer_id"))
|
||||||
@@ -81,3 +84,16 @@ func validateCardExpiryFields(month uint32, year uint32) error {
|
|||||||
}
|
}
|
||||||
return nil
|
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,22 @@ func TestValidateCardPayoutRequest_Errors(t *testing.T) {
|
|||||||
expected string
|
expected string
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "missing_payout_id",
|
name: "missing_operation_identity",
|
||||||
mutate: func(r *mntxv1.CardPayoutRequest) { r.PayoutId = "" },
|
mutate: func(r *mntxv1.CardPayoutRequest) { r.PayoutId = "" },
|
||||||
expected: "missing_payout_id",
|
expected: "missing_operation_ref",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "missing_parent_payment_ref",
|
||||||
|
mutate: func(r *mntxv1.CardPayoutRequest) { r.ParentPaymentRef = "" },
|
||||||
|
expected: "missing_parent_payment_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",
|
name: "missing_customer_id",
|
||||||
|
|||||||
@@ -57,13 +57,44 @@ func mergePayoutStateWithExisting(state, existing *model.CardPayout) {
|
|||||||
if state.IntentRef == "" {
|
if state.IntentRef == "" {
|
||||||
state.IntentRef = existing.IntentRef
|
state.IntentRef = existing.IntentRef
|
||||||
}
|
}
|
||||||
|
if existing.PaymentRef != "" {
|
||||||
|
state.PaymentRef = existing.PaymentRef
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func findOperationRef(operationRef, payoutID string) string {
|
||||||
|
ref := strings.TrimSpace(operationRef)
|
||||||
|
if ref != "" {
|
||||||
|
return ref
|
||||||
|
}
|
||||||
|
return strings.TrimSpace(payoutID)
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *cardPayoutProcessor) findAndMergePayoutState(ctx context.Context, state *model.CardPayout) (*model.CardPayout, error) {
|
func (p *cardPayoutProcessor) findAndMergePayoutState(ctx context.Context, state *model.CardPayout) (*model.CardPayout, error) {
|
||||||
if p == nil || state == nil {
|
if p == nil || state == nil {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
existing, err := p.store.Payouts().FindByPaymentID(ctx, state.PaymentRef)
|
existing, err := p.findExistingPayoutState(ctx, state)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -218,13 +249,15 @@ func (p *cardPayoutProcessor) Submit(ctx context.Context, req *mntxv1.CardPayout
|
|||||||
}
|
}
|
||||||
|
|
||||||
req = sanitizeCardPayoutRequest(req)
|
req = sanitizeCardPayoutRequest(req)
|
||||||
|
operationRef := findOperationRef(req.GetOperationRef(), req.GetPayoutId())
|
||||||
|
parentPaymentRef := strings.TrimSpace(req.GetParentPaymentRef())
|
||||||
|
|
||||||
p.logger.Info("Submitting card payout",
|
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.String("customer_id", strings.TrimSpace(req.GetCustomerId())),
|
||||||
zap.Int64("amount_minor", req.GetAmountMinor()),
|
zap.Int64("amount_minor", req.GetAmountMinor()),
|
||||||
zap.String("currency", strings.ToUpper(strings.TrimSpace(req.GetCurrency()))),
|
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())),
|
zap.String("idempotency_key", strings.TrimSpace(req.GetIdempotencyKey())),
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -235,7 +268,8 @@ func (p *cardPayoutProcessor) Submit(ctx context.Context, req *mntxv1.CardPayout
|
|||||||
|
|
||||||
if err := validateCardPayoutRequest(req, p.config); err != nil {
|
if err := validateCardPayoutRequest(req, p.config); err != nil {
|
||||||
p.logger.Warn("Card payout validation failed",
|
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.String("customer_id", req.GetCustomerId()),
|
||||||
zap.Error(err),
|
zap.Error(err),
|
||||||
)
|
)
|
||||||
@@ -243,7 +277,8 @@ func (p *cardPayoutProcessor) Submit(ctx context.Context, req *mntxv1.CardPayout
|
|||||||
}
|
}
|
||||||
if err := p.validatePerTxMinimum(req.GetAmountMinor(), req.GetCurrency()); err != nil {
|
if err := p.validatePerTxMinimum(req.GetAmountMinor(), req.GetCurrency()); err != nil {
|
||||||
p.logger.Warn("Card payout amount below configured minimum",
|
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.String("customer_id", req.GetCustomerId()),
|
||||||
zap.Int64("amount_minor", req.GetAmountMinor()),
|
zap.Int64("amount_minor", req.GetAmountMinor()),
|
||||||
zap.String("currency", strings.ToUpper(strings.TrimSpace(req.GetCurrency()))),
|
zap.String("currency", strings.ToUpper(strings.TrimSpace(req.GetCurrency()))),
|
||||||
@@ -253,7 +288,7 @@ func (p *cardPayoutProcessor) Submit(ctx context.Context, req *mntxv1.CardPayout
|
|||||||
return nil, err
|
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 {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -264,8 +299,8 @@ func (p *cardPayoutProcessor) Submit(ctx context.Context, req *mntxv1.CardPayout
|
|||||||
Base: storable.Base{
|
Base: storable.Base{
|
||||||
ID: bson.NilObjectID,
|
ID: bson.NilObjectID,
|
||||||
},
|
},
|
||||||
PaymentRef: strings.TrimSpace(req.GetPayoutId()),
|
PaymentRef: parentPaymentRef,
|
||||||
OperationRef: strings.TrimSpace(req.GetOperationRef()),
|
OperationRef: operationRef,
|
||||||
IdempotencyKey: strings.TrimSpace(req.GetIdempotencyKey()),
|
IdempotencyKey: strings.TrimSpace(req.GetIdempotencyKey()),
|
||||||
IntentRef: strings.TrimSpace(req.GetIntentRef()),
|
IntentRef: strings.TrimSpace(req.GetIntentRef()),
|
||||||
ProjectID: projectID,
|
ProjectID: projectID,
|
||||||
@@ -339,13 +374,15 @@ func (p *cardPayoutProcessor) SubmitToken(ctx context.Context, req *mntxv1.CardT
|
|||||||
}
|
}
|
||||||
|
|
||||||
req = sanitizeCardTokenPayoutRequest(req)
|
req = sanitizeCardTokenPayoutRequest(req)
|
||||||
|
operationRef := findOperationRef(req.GetOperationRef(), req.GetPayoutId())
|
||||||
|
parentPaymentRef := strings.TrimSpace(req.GetParentPaymentRef())
|
||||||
|
|
||||||
p.logger.Info("Submitting card token payout",
|
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.String("customer_id", strings.TrimSpace(req.GetCustomerId())),
|
||||||
zap.Int64("amount_minor", req.GetAmountMinor()),
|
zap.Int64("amount_minor", req.GetAmountMinor()),
|
||||||
zap.String("currency", strings.ToUpper(strings.TrimSpace(req.GetCurrency()))),
|
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())),
|
zap.String("idempotency_key", strings.TrimSpace(req.GetIdempotencyKey())),
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -356,7 +393,8 @@ func (p *cardPayoutProcessor) SubmitToken(ctx context.Context, req *mntxv1.CardT
|
|||||||
|
|
||||||
if err := validateCardTokenPayoutRequest(req, p.config); err != nil {
|
if err := validateCardTokenPayoutRequest(req, p.config); err != nil {
|
||||||
p.logger.Warn("Card token payout validation failed",
|
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.String("customer_id", req.GetCustomerId()),
|
||||||
zap.Error(err),
|
zap.Error(err),
|
||||||
)
|
)
|
||||||
@@ -364,7 +402,8 @@ func (p *cardPayoutProcessor) SubmitToken(ctx context.Context, req *mntxv1.CardT
|
|||||||
}
|
}
|
||||||
if err := p.validatePerTxMinimum(req.GetAmountMinor(), req.GetCurrency()); err != nil {
|
if err := p.validatePerTxMinimum(req.GetAmountMinor(), req.GetCurrency()); err != nil {
|
||||||
p.logger.Warn("Card token payout amount below configured minimum",
|
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.String("customer_id", req.GetCustomerId()),
|
||||||
zap.Int64("amount_minor", req.GetAmountMinor()),
|
zap.Int64("amount_minor", req.GetAmountMinor()),
|
||||||
zap.String("currency", strings.ToUpper(strings.TrimSpace(req.GetCurrency()))),
|
zap.String("currency", strings.ToUpper(strings.TrimSpace(req.GetCurrency()))),
|
||||||
@@ -374,16 +413,17 @@ func (p *cardPayoutProcessor) SubmitToken(ctx context.Context, req *mntxv1.CardT
|
|||||||
return nil, err
|
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 {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
now := p.clock.Now()
|
now := p.clock.Now()
|
||||||
state := &model.CardPayout{
|
state := &model.CardPayout{
|
||||||
PaymentRef: strings.TrimSpace(req.GetPayoutId()),
|
PaymentRef: parentPaymentRef,
|
||||||
OperationRef: strings.TrimSpace(req.GetOperationRef()),
|
OperationRef: operationRef,
|
||||||
IdempotencyKey: strings.TrimSpace(req.GetIdempotencyKey()),
|
IdempotencyKey: strings.TrimSpace(req.GetIdempotencyKey()),
|
||||||
|
IntentRef: strings.TrimSpace(req.GetIntentRef()),
|
||||||
ProjectID: projectID,
|
ProjectID: projectID,
|
||||||
CustomerID: strings.TrimSpace(req.GetCustomerId()),
|
CustomerID: strings.TrimSpace(req.GetCustomerId()),
|
||||||
AmountMinor: req.GetAmountMinor(),
|
AmountMinor: req.GetAmountMinor(),
|
||||||
@@ -509,21 +549,26 @@ func (p *cardPayoutProcessor) Status(ctx context.Context, payoutID string) (*mnt
|
|||||||
}
|
}
|
||||||
|
|
||||||
id := strings.TrimSpace(payoutID)
|
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_ref", id))
|
||||||
|
|
||||||
if id == "" {
|
if id == "" {
|
||||||
p.logger.Warn("Payout status requested with empty payout_id")
|
p.logger.Warn("Payout status requested with empty payout_id")
|
||||||
return nil, merrors.InvalidArgument("payout_id is required", "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 || state == nil {
|
if err != nil && !errors.Is(err, merrors.ErrNoData) {
|
||||||
p.logger.Warn("Payout status not found", zap.String("payout_id", id), zap.Error(err))
|
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) {
|
||||||
|
p.logger.Warn("Payout status not found", zap.String("operation_ref", id))
|
||||||
return nil, merrors.NoData("payout not found")
|
return nil, merrors.NoData("payout not found")
|
||||||
}
|
}
|
||||||
|
|
||||||
p.logger.Info("Card payout status resolved",
|
p.logger.Info("Card payout status resolved",
|
||||||
zap.String("payment_ref", state.PaymentRef),
|
zap.String("payment_ref", state.PaymentRef),
|
||||||
|
zap.String("operation_ref", state.OperationRef),
|
||||||
zap.String("status", string(state.Status)),
|
zap.String("status", string(state.Status)),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"testing"
|
"testing"
|
||||||
@@ -45,7 +46,8 @@ func TestCardPayoutProcessor_Submit_Success(t *testing.T) {
|
|||||||
|
|
||||||
repo := newMockRepository()
|
repo := newMockRepository()
|
||||||
repo.payouts.Save(&model.CardPayout{
|
repo.payouts.Save(&model.CardPayout{
|
||||||
PaymentRef: "payout-1",
|
PaymentRef: "payment-parent-1",
|
||||||
|
OperationRef: "payout-1",
|
||||||
CreatedAt: existingCreated,
|
CreatedAt: existingCreated,
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -227,3 +229,173 @@ func TestCardPayoutProcessor_ProcessCallback(t *testing.T) {
|
|||||||
t.Fatalf("expected success status in model, got %v", state.Status)
|
t.Fatalf("expected success status in model, got %v", state.Status)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestCardPayoutProcessor_Submit_SameParentDifferentOperationsStoredSeparately(t *testing.T) {
|
||||||
|
cfg := monetix.Config{
|
||||||
|
BaseURL: "https://monetix.test",
|
||||||
|
SecretKey: "secret",
|
||||||
|
ProjectID: 99,
|
||||||
|
AllowedCurrencies: []string{"RUB"},
|
||||||
|
}
|
||||||
|
|
||||||
|
repo := newMockRepository()
|
||||||
|
var callN int
|
||||||
|
httpClient := &http.Client{
|
||||||
|
Transport: roundTripperFunc(func(r *http.Request) (*http.Response, error) {
|
||||||
|
callN++
|
||||||
|
resp := monetix.APIResponse{}
|
||||||
|
resp.Operation.RequestID = fmt.Sprintf("req-%d", callN)
|
||||||
|
body, _ := json.Marshal(resp)
|
||||||
|
return &http.Response{
|
||||||
|
StatusCode: http.StatusOK,
|
||||||
|
Body: io.NopCloser(bytes.NewReader(body)),
|
||||||
|
Header: http.Header{"Content-Type": []string{"application/json"}},
|
||||||
|
}, nil
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
processor := newCardPayoutProcessor(
|
||||||
|
zap.NewNop(),
|
||||||
|
cfg,
|
||||||
|
staticClock{now: time.Date(2026, 3, 4, 1, 2, 3, 0, time.UTC)},
|
||||||
|
repo,
|
||||||
|
httpClient,
|
||||||
|
nil,
|
||||||
|
)
|
||||||
|
|
||||||
|
parentPaymentRef := "payment-parent-1"
|
||||||
|
op1 := parentPaymentRef + ":hop_4_card_payout_send"
|
||||||
|
op2 := parentPaymentRef + ":hop_4_card_payout_send_2"
|
||||||
|
|
||||||
|
req1 := validCardPayoutRequest()
|
||||||
|
req1.PayoutId = ""
|
||||||
|
req1.OperationRef = op1
|
||||||
|
req1.IdempotencyKey = "idem-1"
|
||||||
|
req1.ParentPaymentRef = parentPaymentRef
|
||||||
|
req1.CardPan = "2204310000002456"
|
||||||
|
|
||||||
|
req2 := validCardPayoutRequest()
|
||||||
|
req2.PayoutId = ""
|
||||||
|
req2.OperationRef = op2
|
||||||
|
req2.IdempotencyKey = "idem-2"
|
||||||
|
req2.ParentPaymentRef = parentPaymentRef
|
||||||
|
req2.CardPan = "2204320000009754"
|
||||||
|
|
||||||
|
if _, err := processor.Submit(context.Background(), req1); err != nil {
|
||||||
|
t.Fatalf("first submit failed: %v", err)
|
||||||
|
}
|
||||||
|
if _, err := processor.Submit(context.Background(), req2); err != nil {
|
||||||
|
t.Fatalf("second submit failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
first, err := repo.payouts.FindByOperationRef(context.Background(), op1)
|
||||||
|
if err != nil || first == nil {
|
||||||
|
t.Fatalf("expected first operation stored, err=%v", err)
|
||||||
|
}
|
||||||
|
second, err := repo.payouts.FindByOperationRef(context.Background(), op2)
|
||||||
|
if err != nil || second == nil {
|
||||||
|
t.Fatalf("expected second operation stored, err=%v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if got, want := first.PaymentRef, parentPaymentRef; got != want {
|
||||||
|
t.Fatalf("first parent payment ref mismatch: got=%q want=%q", got, want)
|
||||||
|
}
|
||||||
|
if got, want := second.PaymentRef, parentPaymentRef; got != want {
|
||||||
|
t.Fatalf("second parent payment ref mismatch: got=%q want=%q", got, want)
|
||||||
|
}
|
||||||
|
if got, want := first.OperationRef, op1; got != want {
|
||||||
|
t.Fatalf("first operation ref mismatch: got=%q want=%q", got, want)
|
||||||
|
}
|
||||||
|
if got, want := second.OperationRef, op2; got != want {
|
||||||
|
t.Fatalf("second operation ref mismatch: got=%q want=%q", got, want)
|
||||||
|
}
|
||||||
|
if first.ProviderPaymentID == "" || second.ProviderPaymentID == "" {
|
||||||
|
t.Fatalf("expected provider payment ids for both operations")
|
||||||
|
}
|
||||||
|
if first.ProviderPaymentID == second.ProviderPaymentID {
|
||||||
|
t.Fatalf("expected different provider payment ids, got=%q", first.ProviderPaymentID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCardPayoutProcessor_ProcessCallback_UpdatesMatchingOperationWithinSameParent(t *testing.T) {
|
||||||
|
cfg := monetix.Config{
|
||||||
|
SecretKey: "secret",
|
||||||
|
StatusSuccess: "success",
|
||||||
|
StatusProcessing: "processing",
|
||||||
|
AllowedCurrencies: []string{"RUB"},
|
||||||
|
}
|
||||||
|
|
||||||
|
parentPaymentRef := "payment-parent-1"
|
||||||
|
op1 := parentPaymentRef + ":hop_4_card_payout_send"
|
||||||
|
op2 := parentPaymentRef + ":hop_4_card_payout_send_2"
|
||||||
|
now := time.Date(2026, 3, 4, 2, 3, 4, 0, time.UTC)
|
||||||
|
|
||||||
|
repo := newMockRepository()
|
||||||
|
repo.payouts.Save(&model.CardPayout{
|
||||||
|
PaymentRef: parentPaymentRef,
|
||||||
|
OperationRef: op1,
|
||||||
|
Status: model.PayoutStatusWaiting,
|
||||||
|
CreatedAt: now.Add(-time.Minute),
|
||||||
|
UpdatedAt: now.Add(-time.Minute),
|
||||||
|
})
|
||||||
|
repo.payouts.Save(&model.CardPayout{
|
||||||
|
PaymentRef: parentPaymentRef,
|
||||||
|
OperationRef: op2,
|
||||||
|
Status: model.PayoutStatusWaiting,
|
||||||
|
CreatedAt: now.Add(-time.Minute),
|
||||||
|
UpdatedAt: now.Add(-time.Minute),
|
||||||
|
})
|
||||||
|
|
||||||
|
processor := newCardPayoutProcessor(
|
||||||
|
zap.NewNop(),
|
||||||
|
cfg,
|
||||||
|
staticClock{now: now},
|
||||||
|
repo,
|
||||||
|
&http.Client{},
|
||||||
|
nil,
|
||||||
|
)
|
||||||
|
|
||||||
|
cb := baseCallback()
|
||||||
|
cb.Payment.ID = op2
|
||||||
|
cb.Payment.Status = "success"
|
||||||
|
cb.Operation.Status = "success"
|
||||||
|
cb.Operation.Code = "0"
|
||||||
|
cb.Operation.Provider.PaymentID = "provider-op-2"
|
||||||
|
cb.Payment.Sum.Currency = "RUB"
|
||||||
|
|
||||||
|
sig, err := monetix.SignPayload(cb, cfg.SecretKey)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to sign callback: %v", err)
|
||||||
|
}
|
||||||
|
cb.Signature = sig
|
||||||
|
payload, err := json.Marshal(cb)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to marshal callback: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
status, err := processor.ProcessCallback(context.Background(), payload)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("expected no error, got %v", err)
|
||||||
|
}
|
||||||
|
if status != http.StatusOK {
|
||||||
|
t.Fatalf("expected status ok, got %d", status)
|
||||||
|
}
|
||||||
|
|
||||||
|
first, err := repo.payouts.FindByOperationRef(context.Background(), op1)
|
||||||
|
if err != nil || first == nil {
|
||||||
|
t.Fatalf("expected first operation present, err=%v", err)
|
||||||
|
}
|
||||||
|
second, err := repo.payouts.FindByOperationRef(context.Background(), op2)
|
||||||
|
if err != nil || second == nil {
|
||||||
|
t.Fatalf("expected second operation present, err=%v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if got, want := first.Status, model.PayoutStatusWaiting; got != want {
|
||||||
|
t.Fatalf("first operation status mismatch: got=%v want=%v", got, want)
|
||||||
|
}
|
||||||
|
if got, want := second.Status, model.PayoutStatusSuccess; got != want {
|
||||||
|
t.Fatalf("second operation status mismatch: got=%v want=%v", got, want)
|
||||||
|
}
|
||||||
|
if got, want := second.PaymentRef, parentPaymentRef; got != want {
|
||||||
|
t.Fatalf("second parent payment ref mismatch: got=%q want=%q", got, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -13,8 +13,11 @@ func validateCardTokenPayoutRequest(req *mntxv1.CardTokenPayoutRequest, cfg mone
|
|||||||
return newPayoutError("invalid_request", merrors.InvalidArgument("request cannot be empty"))
|
return newPayoutError("invalid_request", merrors.InvalidArgument("request cannot be empty"))
|
||||||
}
|
}
|
||||||
|
|
||||||
if strings.TrimSpace(req.GetPayoutId()) == "" {
|
if err := validateOperationIdentity(strings.TrimSpace(req.GetPayoutId()), strings.TrimSpace(req.GetOperationRef())); err != nil {
|
||||||
return newPayoutError("missing_payout_id", merrors.InvalidArgument("payout_id is required", "payout_id"))
|
return err
|
||||||
|
}
|
||||||
|
if strings.TrimSpace(req.GetParentPaymentRef()) == "" {
|
||||||
|
return newPayoutError("missing_parent_payment_ref", merrors.InvalidArgument("parent_payment_ref is required", "parent_payment_ref"))
|
||||||
}
|
}
|
||||||
if strings.TrimSpace(req.GetCustomerId()) == "" {
|
if strings.TrimSpace(req.GetCustomerId()) == "" {
|
||||||
return newPayoutError("missing_customer_id", merrors.InvalidArgument("customer_id is required", "customer_id"))
|
return newPayoutError("missing_customer_id", merrors.InvalidArgument("customer_id is required", "customer_id"))
|
||||||
|
|||||||
@@ -24,9 +24,22 @@ func TestValidateCardTokenPayoutRequest_Errors(t *testing.T) {
|
|||||||
expected string
|
expected string
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "missing_payout_id",
|
name: "missing_operation_identity",
|
||||||
mutate: func(r *mntxv1.CardTokenPayoutRequest) { r.PayoutId = "" },
|
mutate: func(r *mntxv1.CardTokenPayoutRequest) { r.PayoutId = "" },
|
||||||
expected: "missing_payout_id",
|
expected: "missing_operation_ref",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "missing_parent_payment_ref",
|
||||||
|
mutate: func(r *mntxv1.CardTokenPayoutRequest) { r.ParentPaymentRef = "" },
|
||||||
|
expected: "missing_parent_payment_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",
|
name: "missing_customer_id",
|
||||||
|
|||||||
@@ -69,20 +69,18 @@ func (s *Service) SubmitOperation(ctx context.Context, req *connectorv1.SubmitOp
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return &connectorv1.SubmitOperationResponse{Receipt: &connectorv1.OperationReceipt{Error: connectorError(connectorv1.ErrorCode_INVALID_PARAMS, err.Error(), op, "")}}, nil
|
return &connectorv1.SubmitOperationResponse{Receipt: &connectorv1.OperationReceipt{Error: connectorError(connectorv1.ErrorCode_INVALID_PARAMS, err.Error(), op, "")}}, nil
|
||||||
}
|
}
|
||||||
|
parentPaymentRef := strings.TrimSpace(reader.String("parent_payment_ref"))
|
||||||
|
|
||||||
payoutID := strings.TrimSpace(reader.String("payout_id"))
|
payoutID := operationIDForRequest(operationRef)
|
||||||
if payoutID == "" {
|
|
||||||
payoutID = strings.TrimSpace(op.GetIdempotencyKey())
|
|
||||||
}
|
|
||||||
|
|
||||||
if strings.TrimSpace(reader.String("card_token")) != "" {
|
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, parentPaymentRef, idempotencyKey, operationRef, intentRef, amountMinor, currency))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return &connectorv1.SubmitOperationResponse{Receipt: &connectorv1.OperationReceipt{Error: connectorError(mapErrorCode(err), err.Error(), op, "")}}, nil
|
return &connectorv1.SubmitOperationResponse{Receipt: &connectorv1.OperationReceipt{Error: connectorError(mapErrorCode(err), err.Error(), op, "")}}, nil
|
||||||
}
|
}
|
||||||
return &connectorv1.SubmitOperationResponse{Receipt: payoutReceipt(resp.GetPayout())}, nil
|
return &connectorv1.SubmitOperationResponse{Receipt: payoutReceipt(resp.GetPayout())}, nil
|
||||||
}
|
}
|
||||||
cr := buildCardPayoutRequestFromParams(reader, payoutID, idempotencyKey, operationRef, intentRef, amountMinor, currency)
|
cr := buildCardPayoutRequestFromParams(reader, payoutID, parentPaymentRef, idempotencyKey, operationRef, intentRef, amountMinor, currency)
|
||||||
resp, err := s.CreateCardPayout(ctx, cr)
|
resp, err := s.CreateCardPayout(ctx, cr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return &connectorv1.SubmitOperationResponse{Receipt: &connectorv1.OperationReceipt{Error: connectorError(mapErrorCode(err), err.Error(), op, "")}}, nil
|
return &connectorv1.SubmitOperationResponse{Receipt: &connectorv1.OperationReceipt{Error: connectorError(mapErrorCode(err), err.Error(), op, "")}}, nil
|
||||||
@@ -119,6 +117,7 @@ func mntxOperationParams() []*connectorv1.OperationParamSpec {
|
|||||||
{Key: "card_holder", Type: connectorv1.ParamType_STRING, Required: false},
|
{Key: "card_holder", Type: connectorv1.ParamType_STRING, Required: false},
|
||||||
{Key: "amount_minor", Type: connectorv1.ParamType_INT, Required: false},
|
{Key: "amount_minor", Type: connectorv1.ParamType_INT, Required: false},
|
||||||
{Key: "project_id", 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: "customer_middle_name", 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_country", Type: connectorv1.ParamType_STRING, Required: false},
|
||||||
{Key: "customer_state", Type: connectorv1.ParamType_STRING, Required: false},
|
{Key: "customer_state", Type: connectorv1.ParamType_STRING, Required: false},
|
||||||
@@ -169,9 +168,29 @@ func currencyFromOperation(op *connectorv1.Operation) string {
|
|||||||
return strings.ToUpper(currency)
|
return strings.ToUpper(currency)
|
||||||
}
|
}
|
||||||
|
|
||||||
func buildCardTokenPayoutRequestFromParams(reader params.Reader, payoutID string, amountMinor int64, currency string) *mntxv1.CardTokenPayoutRequest {
|
func operationIDForRequest(operationRef string) string {
|
||||||
|
return strings.TrimSpace(operationRef)
|
||||||
|
}
|
||||||
|
|
||||||
|
func metadataFromReader(reader params.Reader) map[string]string {
|
||||||
|
metadata := reader.StringMap("metadata")
|
||||||
|
if len(metadata) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return metadata
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildCardTokenPayoutRequestFromParams(reader params.Reader,
|
||||||
|
payoutID, parentPaymentRef, idempotencyKey, operationRef, intentRef string,
|
||||||
|
amountMinor int64, currency string) *mntxv1.CardTokenPayoutRequest {
|
||||||
|
operationRef = strings.TrimSpace(operationRef)
|
||||||
|
payoutID = strings.TrimSpace(payoutID)
|
||||||
|
if operationRef != "" {
|
||||||
|
payoutID = ""
|
||||||
|
}
|
||||||
req := &mntxv1.CardTokenPayoutRequest{
|
req := &mntxv1.CardTokenPayoutRequest{
|
||||||
PayoutId: payoutID,
|
PayoutId: payoutID,
|
||||||
|
ParentPaymentRef: strings.TrimSpace(parentPaymentRef),
|
||||||
ProjectId: readerInt64(reader, "project_id"),
|
ProjectId: readerInt64(reader, "project_id"),
|
||||||
CustomerId: strings.TrimSpace(reader.String("customer_id")),
|
CustomerId: strings.TrimSpace(reader.String("customer_id")),
|
||||||
CustomerFirstName: strings.TrimSpace(reader.String("customer_first_name")),
|
CustomerFirstName: strings.TrimSpace(reader.String("customer_first_name")),
|
||||||
@@ -188,16 +207,25 @@ func buildCardTokenPayoutRequestFromParams(reader params.Reader, payoutID string
|
|||||||
CardToken: strings.TrimSpace(reader.String("card_token")),
|
CardToken: strings.TrimSpace(reader.String("card_token")),
|
||||||
CardHolder: strings.TrimSpace(reader.String("card_holder")),
|
CardHolder: strings.TrimSpace(reader.String("card_holder")),
|
||||||
MaskedPan: strings.TrimSpace(reader.String("masked_pan")),
|
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
|
return req
|
||||||
}
|
}
|
||||||
|
|
||||||
func buildCardPayoutRequestFromParams(reader params.Reader,
|
func buildCardPayoutRequestFromParams(reader params.Reader,
|
||||||
payoutID, idempotencyKey, operationRef, intentRef string,
|
payoutID, parentPaymentRef, idempotencyKey, operationRef, intentRef string,
|
||||||
amountMinor int64, currency string) *mntxv1.CardPayoutRequest {
|
amountMinor int64, currency string) *mntxv1.CardPayoutRequest {
|
||||||
|
operationRef = strings.TrimSpace(operationRef)
|
||||||
|
payoutID = strings.TrimSpace(payoutID)
|
||||||
|
if operationRef != "" {
|
||||||
|
payoutID = ""
|
||||||
|
}
|
||||||
return &mntxv1.CardPayoutRequest{
|
return &mntxv1.CardPayoutRequest{
|
||||||
PayoutId: payoutID,
|
PayoutId: payoutID,
|
||||||
|
ParentPaymentRef: strings.TrimSpace(parentPaymentRef),
|
||||||
ProjectId: readerInt64(reader, "project_id"),
|
ProjectId: readerInt64(reader, "project_id"),
|
||||||
CustomerId: strings.TrimSpace(reader.String("customer_id")),
|
CustomerId: strings.TrimSpace(reader.String("customer_id")),
|
||||||
CustomerFirstName: strings.TrimSpace(reader.String("customer_first_name")),
|
CustomerFirstName: strings.TrimSpace(reader.String("customer_first_name")),
|
||||||
@@ -215,10 +243,10 @@ func buildCardPayoutRequestFromParams(reader params.Reader,
|
|||||||
CardExpYear: uint32(readerInt64(reader, "card_exp_year")),
|
CardExpYear: uint32(readerInt64(reader, "card_exp_year")),
|
||||||
CardExpMonth: uint32(readerInt64(reader, "card_exp_month")),
|
CardExpMonth: uint32(readerInt64(reader, "card_exp_month")),
|
||||||
CardHolder: strings.TrimSpace(reader.String("card_holder")),
|
CardHolder: strings.TrimSpace(reader.String("card_holder")),
|
||||||
Metadata: reader.StringMap("metadata"),
|
Metadata: metadataFromReader(reader),
|
||||||
OperationRef: operationRef,
|
OperationRef: operationRef,
|
||||||
IdempotencyKey: idempotencyKey,
|
IdempotencyKey: strings.TrimSpace(idempotencyKey),
|
||||||
IntentRef: intentRef,
|
IntentRef: strings.TrimSpace(intentRef),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -236,7 +264,7 @@ func payoutReceipt(state *mntxv1.CardPayoutState) *connectorv1.OperationReceipt
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
return &connectorv1.OperationReceipt{
|
return &connectorv1.OperationReceipt{
|
||||||
OperationId: strings.TrimSpace(state.GetPayoutId()),
|
OperationId: firstNonEmpty(strings.TrimSpace(state.GetOperationRef()), strings.TrimSpace(state.GetPayoutId())),
|
||||||
Status: payoutStatusToOperation(state.GetStatus()),
|
Status: payoutStatusToOperation(state.GetStatus()),
|
||||||
ProviderRef: strings.TrimSpace(state.GetProviderPaymentId()),
|
ProviderRef: strings.TrimSpace(state.GetProviderPaymentId()),
|
||||||
}
|
}
|
||||||
@@ -247,7 +275,7 @@ func payoutToOperation(state *mntxv1.CardPayoutState) *connectorv1.Operation {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
return &connectorv1.Operation{
|
return &connectorv1.Operation{
|
||||||
OperationId: strings.TrimSpace(state.GetPayoutId()),
|
OperationId: firstNonEmpty(strings.TrimSpace(state.GetOperationRef()), strings.TrimSpace(state.GetPayoutId())),
|
||||||
Type: connectorv1.OperationType_PAYOUT,
|
Type: connectorv1.OperationType_PAYOUT,
|
||||||
Status: payoutStatusToOperation(state.GetStatus()),
|
Status: payoutStatusToOperation(state.GetStatus()),
|
||||||
Money: &moneyv1.Money{
|
Money: &moneyv1.Money{
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package gateway
|
package gateway
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/tech/sendico/gateway/mntx/storage/model"
|
"github.com/tech/sendico/gateway/mntx/storage/model"
|
||||||
@@ -22,7 +23,7 @@ func CardPayoutStateFromProto(clock clockpkg.Clock, p *mntxv1.CardPayoutState) *
|
|||||||
}
|
}
|
||||||
|
|
||||||
return &model.CardPayout{
|
return &model.CardPayout{
|
||||||
PaymentRef: p.PayoutId,
|
PaymentRef: strings.TrimSpace(p.GetParentPaymentRef()),
|
||||||
OperationRef: p.GetOperationRef(),
|
OperationRef: p.GetOperationRef(),
|
||||||
IntentRef: p.GetIntentRef(),
|
IntentRef: p.GetIntentRef(),
|
||||||
IdempotencyKey: p.GetIdempotencyKey(),
|
IdempotencyKey: p.GetIdempotencyKey(),
|
||||||
@@ -41,7 +42,8 @@ func CardPayoutStateFromProto(clock clockpkg.Clock, p *mntxv1.CardPayoutState) *
|
|||||||
|
|
||||||
func StateToProto(m *model.CardPayout) *mntxv1.CardPayoutState {
|
func StateToProto(m *model.CardPayout) *mntxv1.CardPayoutState {
|
||||||
return &mntxv1.CardPayoutState{
|
return &mntxv1.CardPayoutState{
|
||||||
PayoutId: m.PaymentRef,
|
PayoutId: firstNonEmpty(m.OperationRef, m.PaymentRef),
|
||||||
|
ParentPaymentRef: m.PaymentRef,
|
||||||
ProjectId: m.ProjectID,
|
ProjectId: m.ProjectID,
|
||||||
CustomerId: m.CustomerID,
|
CustomerId: m.CustomerID,
|
||||||
AmountMinor: m.AmountMinor,
|
AmountMinor: m.AmountMinor,
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ func testMonetixConfig() monetix.Config {
|
|||||||
func validCardPayoutRequest() *mntxv1.CardPayoutRequest {
|
func validCardPayoutRequest() *mntxv1.CardPayoutRequest {
|
||||||
return &mntxv1.CardPayoutRequest{
|
return &mntxv1.CardPayoutRequest{
|
||||||
PayoutId: "payout-1",
|
PayoutId: "payout-1",
|
||||||
|
ParentPaymentRef: "payment-parent-1",
|
||||||
CustomerId: "cust-1",
|
CustomerId: "cust-1",
|
||||||
CustomerFirstName: "Jane",
|
CustomerFirstName: "Jane",
|
||||||
CustomerLastName: "Doe",
|
CustomerLastName: "Doe",
|
||||||
@@ -52,6 +53,7 @@ func validCardPayoutRequest() *mntxv1.CardPayoutRequest {
|
|||||||
func validCardTokenPayoutRequest() *mntxv1.CardTokenPayoutRequest {
|
func validCardTokenPayoutRequest() *mntxv1.CardTokenPayoutRequest {
|
||||||
return &mntxv1.CardTokenPayoutRequest{
|
return &mntxv1.CardTokenPayoutRequest{
|
||||||
PayoutId: "payout-1",
|
PayoutId: "payout-1",
|
||||||
|
ParentPaymentRef: "payment-parent-1",
|
||||||
CustomerId: "cust-1",
|
CustomerId: "cust-1",
|
||||||
CustomerFirstName: "Jane",
|
CustomerFirstName: "Jane",
|
||||||
CustomerLastName: "Doe",
|
CustomerLastName: "Doe",
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ const (
|
|||||||
payoutsCollection = "card_payouts"
|
payoutsCollection = "card_payouts"
|
||||||
payoutIdemField = "idempotencyKey"
|
payoutIdemField = "idempotencyKey"
|
||||||
payoutIdField = "paymentRef"
|
payoutIdField = "paymentRef"
|
||||||
|
payoutOpField = "operationRef"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Payouts struct {
|
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))
|
logger = logger.Named("payouts").With(zap.String("collection", payoutsCollection))
|
||||||
|
|
||||||
repo := repository.CreateMongoRepository(db, 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{
|
if err := repo.CreateIndex(&ri.Definition{
|
||||||
Keys: []ri.Key{{Field: payoutIdemField, Sort: ri.Asc}},
|
Keys: []ri.Key{{Field: payoutIdemField, Sort: ri.Asc}},
|
||||||
Unique: true,
|
Unique: true,
|
||||||
@@ -63,6 +74,10 @@ func (p *Payouts) FindByIdempotencyKey(ctx context.Context, key string) (*model.
|
|||||||
return p.findOneByField(ctx, payoutIdemField, key)
|
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) {
|
func (p *Payouts) FindByPaymentID(ctx context.Context, paymentID string) (*model.CardPayout, error) {
|
||||||
return p.findOneByField(ctx, payoutIdField, paymentID)
|
return p.findOneByField(ctx, payoutIdField, paymentID)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ type Repository interface {
|
|||||||
|
|
||||||
type PayoutsStore interface {
|
type PayoutsStore interface {
|
||||||
FindByIdempotencyKey(ctx context.Context, key string) (*model.CardPayout, error)
|
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)
|
FindByPaymentID(ctx context.Context, key string) (*model.CardPayout, error)
|
||||||
Upsert(ctx context.Context, record *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)
|
stepToken := cardPayoutStepToken(req.Step)
|
||||||
operationRef := cardPayoutOperationRef(req.Payment, stepToken)
|
operationRef := cardPayoutOperationRef(req.Payment, stepToken)
|
||||||
payoutRef := cardPayoutRef(req.Payment)
|
|
||||||
idempotencyKey := cardPayoutIdempotencyKey(req.Payment, stepToken)
|
idempotencyKey := cardPayoutIdempotencyKey(req.Payment, stepToken)
|
||||||
projectID := cardPayoutProjectID(req.Payment)
|
projectID := cardPayoutProjectID(req.Payment)
|
||||||
customer := cardPayoutCustomerFromPayment(req.Payment, card, req.Step.Metadata)
|
customer := cardPayoutCustomerFromPayment(req.Payment, card, req.Step.Metadata)
|
||||||
@@ -85,8 +84,8 @@ func (e *gatewayCardPayoutExecutor) ExecuteCardPayout(ctx context.Context, req s
|
|||||||
var responsePayout *mntxv1.CardPayoutState
|
var responsePayout *mntxv1.CardPayoutState
|
||||||
if token := strings.TrimSpace(card.Token); token != "" {
|
if token := strings.TrimSpace(card.Token); token != "" {
|
||||||
resp, createErr := client.CreateCardTokenPayout(ctx, &mntxv1.CardTokenPayoutRequest{
|
resp, createErr := client.CreateCardTokenPayout(ctx, &mntxv1.CardTokenPayoutRequest{
|
||||||
PayoutId: payoutRef,
|
|
||||||
ProjectId: projectID,
|
ProjectId: projectID,
|
||||||
|
ParentPaymentRef: strings.TrimSpace(req.Payment.PaymentRef),
|
||||||
CustomerId: customer.id,
|
CustomerId: customer.id,
|
||||||
CustomerFirstName: customer.firstName,
|
CustomerFirstName: customer.firstName,
|
||||||
CustomerMiddleName: customer.middleName,
|
CustomerMiddleName: customer.middleName,
|
||||||
@@ -123,8 +122,8 @@ func (e *gatewayCardPayoutExecutor) ExecuteCardPayout(ctx context.Context, req s
|
|||||||
return nil, merrors.InvalidArgument("card payout send: card expiry is required")
|
return nil, merrors.InvalidArgument("card payout send: card expiry is required")
|
||||||
}
|
}
|
||||||
resp, createErr := client.CreateCardPayout(ctx, &mntxv1.CardPayoutRequest{
|
resp, createErr := client.CreateCardPayout(ctx, &mntxv1.CardPayoutRequest{
|
||||||
PayoutId: payoutRef,
|
|
||||||
ProjectId: projectID,
|
ProjectId: projectID,
|
||||||
|
ParentPaymentRef: strings.TrimSpace(req.Payment.PaymentRef),
|
||||||
CustomerId: customer.id,
|
CustomerId: customer.id,
|
||||||
CustomerFirstName: customer.firstName,
|
CustomerFirstName: customer.firstName,
|
||||||
CustomerMiddleName: customer.middleName,
|
CustomerMiddleName: customer.middleName,
|
||||||
@@ -155,8 +154,8 @@ func (e *gatewayCardPayoutExecutor) ExecuteCardPayout(ctx context.Context, req s
|
|||||||
responsePayout = resp.GetPayout()
|
responsePayout = resp.GetPayout()
|
||||||
}
|
}
|
||||||
|
|
||||||
resolvedPayoutRef := firstNonEmpty(strings.TrimSpace(responsePayout.GetPayoutId()), payoutRef)
|
|
||||||
resolvedOperationRef := firstNonEmpty(strings.TrimSpace(responsePayout.GetOperationRef()), operationRef)
|
resolvedOperationRef := firstNonEmpty(strings.TrimSpace(responsePayout.GetOperationRef()), operationRef)
|
||||||
|
resolvedPayoutRef := firstNonEmpty(strings.TrimSpace(responsePayout.GetPayoutId()), resolvedOperationRef)
|
||||||
gatewayInstanceID := firstNonEmpty(
|
gatewayInstanceID := firstNonEmpty(
|
||||||
strings.TrimSpace(req.Step.InstanceID),
|
strings.TrimSpace(req.Step.InstanceID),
|
||||||
strings.TrimSpace(gateway.InstanceID),
|
strings.TrimSpace(gateway.InstanceID),
|
||||||
@@ -356,14 +355,6 @@ func cardPayoutOperationRef(payment *agg.Payment, stepToken string) string {
|
|||||||
return joinRef(base, stepToken)
|
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 {
|
func cardPayoutIdempotencyKey(payment *agg.Payment, stepToken string) string {
|
||||||
base := ""
|
base := ""
|
||||||
if payment != nil {
|
if payment != nil {
|
||||||
|
|||||||
@@ -122,7 +122,7 @@ func TestGatewayCardPayoutExecutor_ExecuteCardPayout_SubmitsCardPayout(t *testin
|
|||||||
if got, want := dialAddress, "mntx-gateway:50051"; got != want {
|
if got, want := dialAddress, "mntx-gateway:50051"; got != want {
|
||||||
t.Fatalf("dial address mismatch: got=%q want=%q", 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)
|
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 {
|
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 {
|
if got, want := payoutReq.GetMetadata()[settlementMetadataOutgoingLeg], string(discovery.RailCardPayout); got != want {
|
||||||
t.Fatalf("outgoing_leg metadata mismatch: got=%q want=%q", got, want)
|
t.Fatalf("outgoing_leg metadata 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)
|
||||||
|
}
|
||||||
if len(out.StepExecution.ExternalRefs) != 3 {
|
if len(out.StepExecution.ExternalRefs) != 3 {
|
||||||
t.Fatalf("expected 3 external refs, got=%d", len(out.StepExecution.ExternalRefs))
|
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 {
|
if got, want := payoutReq.GetCurrency(), "RUB"; got != want {
|
||||||
t.Fatalf("currency mismatch: got=%q want=%q", 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 {
|
if got, want := payoutReq.GetCardPan(), "2200700142860162"; got != want {
|
||||||
t.Fatalf("card pan mismatch: got=%q want=%q", got, want)
|
t.Fatalf("card pan mismatch: got=%q want=%q", got, want)
|
||||||
}
|
}
|
||||||
if got, want := payoutReq.GetMetadata()[batchmeta.MetaPayoutTargetRef], "recipient-2"; 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)
|
t.Fatalf("target_ref metadata mismatch: got=%q want=%q", got, want)
|
||||||
}
|
}
|
||||||
|
if got, want := payoutReq.GetParentPaymentRef(), "payment-2"; got != want {
|
||||||
|
t.Fatalf("parent_payment_ref mismatch: got=%q want=%q", got, want)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestGatewayCardPayoutExecutor_ExecuteCardPayout_RequiresGatewayRegistry(t *testing.T) {
|
func TestGatewayCardPayoutExecutor_ExecuteCardPayout_RequiresGatewayRegistry(t *testing.T) {
|
||||||
@@ -310,3 +319,140 @@ func TestGatewayCardPayoutExecutor_ExecuteCardPayout_RequiresGatewayRegistry(t *
|
|||||||
t.Fatalf("unexpected error: %v", err)
|
t.Fatalf("unexpected error: %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestGatewayCardPayoutExecutor_ExecuteCardPayout_BatchChildrenUseDistinctOperationRefsAndSharedParent(t *testing.T) {
|
||||||
|
orgID := bson.NewObjectID()
|
||||||
|
|
||||||
|
var payoutReqs []*mntxv1.CardPayoutRequest
|
||||||
|
executor := &gatewayCardPayoutExecutor{
|
||||||
|
gatewayRegistry: &fakeGatewayRegistry{
|
||||||
|
items: []*model.GatewayInstanceDescriptor{
|
||||||
|
{
|
||||||
|
ID: paymenttypes.DefaultCardsGatewayID,
|
||||||
|
InstanceID: paymenttypes.DefaultCardsGatewayID,
|
||||||
|
Rail: discovery.RailCardPayout,
|
||||||
|
InvokeURI: "grpc://mntx-gateway:50051",
|
||||||
|
IsEnabled: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
dialClient: func(_ context.Context, _ string) (mntxclient.Client, error) {
|
||||||
|
return &mntxclient.Fake{
|
||||||
|
CreateCardPayoutFn: func(_ context.Context, req *mntxv1.CardPayoutRequest) (*mntxv1.CardPayoutResponse, error) {
|
||||||
|
payoutReqs = append(payoutReqs, req)
|
||||||
|
return &mntxv1.CardPayoutResponse{
|
||||||
|
Payout: &mntxv1.CardPayoutState{
|
||||||
|
PayoutId: req.GetOperationRef(),
|
||||||
|
OperationRef: req.GetOperationRef(),
|
||||||
|
},
|
||||||
|
}, nil
|
||||||
|
},
|
||||||
|
}, nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
payment := &agg.Payment{
|
||||||
|
OrganizationBoundBase: pm.OrganizationBoundBase{OrganizationRef: orgID},
|
||||||
|
PaymentRef: "payment-3",
|
||||||
|
IdempotencyKey: "idem-3",
|
||||||
|
QuotationRef: "quote-3",
|
||||||
|
IntentSnapshot: model.PaymentIntent{
|
||||||
|
Ref: "intent-3",
|
||||||
|
Destination: model.PaymentEndpoint{
|
||||||
|
Type: model.EndpointTypeCard,
|
||||||
|
Card: &model.CardEndpoint{
|
||||||
|
Pan: "2200700142860161",
|
||||||
|
ExpMonth: 3,
|
||||||
|
ExpYear: 2030,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Customer: &model.Customer{
|
||||||
|
ID: "cust-3",
|
||||||
|
FirstName: "Stephan",
|
||||||
|
LastName: "Deshevikh",
|
||||||
|
IP: "198.51.100.10",
|
||||||
|
},
|
||||||
|
Amount: &paymenttypes.Money{
|
||||||
|
Amount: "1.000000",
|
||||||
|
Currency: "USDT",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
QuoteSnapshot: &model.PaymentQuoteSnapshot{
|
||||||
|
ExpectedSettlementAmount: &paymenttypes.Money{
|
||||||
|
Amount: "76.50",
|
||||||
|
Currency: "RUB",
|
||||||
|
},
|
||||||
|
QuoteRef: "quote-3",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
firstReq := sexec.StepRequest{
|
||||||
|
Payment: payment,
|
||||||
|
Step: xplan.Step{
|
||||||
|
StepRef: "hop_4_card_payout_send",
|
||||||
|
StepCode: "hop.4.card_payout.send",
|
||||||
|
Action: discovery.RailOperationSend,
|
||||||
|
Rail: discovery.RailCardPayout,
|
||||||
|
Gateway: paymenttypes.DefaultCardsGatewayID,
|
||||||
|
InstanceID: paymenttypes.DefaultCardsGatewayID,
|
||||||
|
},
|
||||||
|
StepExecution: agg.StepExecution{
|
||||||
|
StepRef: "hop_4_card_payout_send",
|
||||||
|
StepCode: "hop.4.card_payout.send",
|
||||||
|
Attempt: 1,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
secondReq := sexec.StepRequest{
|
||||||
|
Payment: payment,
|
||||||
|
Step: xplan.Step{
|
||||||
|
StepRef: "hop_4_card_payout_send_2",
|
||||||
|
StepCode: "hop.4.card_payout.send",
|
||||||
|
Action: discovery.RailOperationSend,
|
||||||
|
Rail: discovery.RailCardPayout,
|
||||||
|
Gateway: paymenttypes.DefaultCardsGatewayID,
|
||||||
|
InstanceID: paymenttypes.DefaultCardsGatewayID,
|
||||||
|
Metadata: map[string]string{
|
||||||
|
batchmeta.MetaPayoutTargetRef: "recipient-2",
|
||||||
|
batchmeta.MetaAmount: "150",
|
||||||
|
batchmeta.MetaCurrency: "RUB",
|
||||||
|
batchmeta.MetaCardPan: "2200700142860162",
|
||||||
|
batchmeta.MetaCardExpMonth: "4",
|
||||||
|
batchmeta.MetaCardExpYear: "2030",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
StepExecution: agg.StepExecution{
|
||||||
|
StepRef: "hop_4_card_payout_send_2",
|
||||||
|
StepCode: "hop.4.card_payout.send",
|
||||||
|
Attempt: 1,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := executor.ExecuteCardPayout(context.Background(), firstReq); err != nil {
|
||||||
|
t.Fatalf("first ExecuteCardPayout returned error: %v", err)
|
||||||
|
}
|
||||||
|
if _, err := executor.ExecuteCardPayout(context.Background(), secondReq); err != nil {
|
||||||
|
t.Fatalf("second ExecuteCardPayout returned error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if got, want := len(payoutReqs), 2; got != want {
|
||||||
|
t.Fatalf("submitted request count mismatch: got=%d want=%d", got, want)
|
||||||
|
}
|
||||||
|
if got, want := payoutReqs[0].GetOperationRef(), "payment-3:hop_4_card_payout_send"; got != want {
|
||||||
|
t.Fatalf("first operation_ref mismatch: got=%q want=%q", got, want)
|
||||||
|
}
|
||||||
|
if got, want := payoutReqs[1].GetOperationRef(), "payment-3:hop_4_card_payout_send_2"; got != want {
|
||||||
|
t.Fatalf("second operation_ref mismatch: got=%q want=%q", got, want)
|
||||||
|
}
|
||||||
|
if payoutReqs[0].GetPayoutId() != "" || payoutReqs[1].GetPayoutId() != "" {
|
||||||
|
t.Fatalf("expected empty payout_id for both child operations")
|
||||||
|
}
|
||||||
|
if got, want := payoutReqs[0].GetParentPaymentRef(), "payment-3"; got != want {
|
||||||
|
t.Fatalf("first parent_payment_ref mismatch: got=%q want=%q", got, want)
|
||||||
|
}
|
||||||
|
if got, want := payoutReqs[1].GetParentPaymentRef(), "payment-3"; got != want {
|
||||||
|
t.Fatalf("second parent_payment_ref mismatch: got=%q want=%q", got, want)
|
||||||
|
}
|
||||||
|
if payoutReqs[0].GetCardPan() == payoutReqs[1].GetCardPan() {
|
||||||
|
t.Fatalf("expected different destination cards across child operations")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ enum PayoutStatus {
|
|||||||
|
|
||||||
// Request to initiate a Monetix card payout.
|
// Request to initiate a Monetix card payout.
|
||||||
message CardPayoutRequest {
|
message CardPayoutRequest {
|
||||||
string payout_id = 1; // internal payout id, mapped to Monetix payment_id
|
string payout_id = 1; // alternate operation id mapped to Monetix payment_id
|
||||||
int64 project_id = 2; // optional override; defaults to configured project id
|
int64 project_id = 2; // optional override; defaults to configured project id
|
||||||
string customer_id = 3;
|
string customer_id = 3;
|
||||||
string customer_first_name = 4;
|
string customer_first_name = 4;
|
||||||
@@ -40,9 +40,10 @@ message CardPayoutRequest {
|
|||||||
uint32 card_exp_month = 17;
|
uint32 card_exp_month = 17;
|
||||||
string card_holder = 18;
|
string card_holder = 18;
|
||||||
map<string, string> metadata = 30;
|
map<string, string> metadata = 30;
|
||||||
string operation_ref = 31;
|
string operation_ref = 31; // preferred operation id mapped to Monetix payment_id
|
||||||
string idempotency_key = 32;
|
string idempotency_key = 32;
|
||||||
string intent_ref = 33;
|
string intent_ref = 33;
|
||||||
|
string parent_payment_ref = 34;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Persisted payout state for retrieval and status updates.
|
// Persisted payout state for retrieval and status updates.
|
||||||
@@ -61,6 +62,7 @@ message CardPayoutState {
|
|||||||
string operation_ref = 12;
|
string operation_ref = 12;
|
||||||
string idempotency_key = 13;
|
string idempotency_key = 13;
|
||||||
string intent_ref = 14;
|
string intent_ref = 14;
|
||||||
|
string parent_payment_ref = 15;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Response returned immediately after submitting a payout to Monetix.
|
// Response returned immediately after submitting a payout to Monetix.
|
||||||
@@ -97,7 +99,7 @@ message ListGatewayInstancesResponse {
|
|||||||
|
|
||||||
// Request to initiate a token-based card payout.
|
// Request to initiate a token-based card payout.
|
||||||
message CardTokenPayoutRequest {
|
message CardTokenPayoutRequest {
|
||||||
string payout_id = 1;
|
string payout_id = 1; // alternate operation id
|
||||||
int64 project_id = 2;
|
int64 project_id = 2;
|
||||||
string customer_id = 3;
|
string customer_id = 3;
|
||||||
|
|
||||||
@@ -119,9 +121,10 @@ message CardTokenPayoutRequest {
|
|||||||
string card_holder = 16;
|
string card_holder = 16;
|
||||||
string masked_pan = 17;
|
string masked_pan = 17;
|
||||||
map<string, string> metadata = 30;
|
map<string, string> metadata = 30;
|
||||||
string operation_ref = 31;
|
string operation_ref = 31; // preferred operation id
|
||||||
string idempotency_key = 32;
|
string idempotency_key = 32;
|
||||||
string intent_ref = 33;
|
string intent_ref = 33;
|
||||||
|
string parent_payment_ref = 34;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Response returned immediately after submitting a token payout to Monetix.
|
// Response returned immediately after submitting a token payout to Monetix.
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ import 'package:pshared/provider/payment/flow.dart';
|
|||||||
import 'package:pshared/provider/recipient/provider.dart';
|
import 'package:pshared/provider/recipient/provider.dart';
|
||||||
import 'package:pshared/utils/payment/fx_helpers.dart';
|
import 'package:pshared/utils/payment/fx_helpers.dart';
|
||||||
|
|
||||||
|
|
||||||
class QuotationIntentBuilder {
|
class QuotationIntentBuilder {
|
||||||
static const String _settlementCurrency = 'RUB';
|
static const String _settlementCurrency = 'RUB';
|
||||||
|
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ import 'package:pshared/service/payment/quotation.dart';
|
|||||||
import 'package:pshared/utils/payment/quote_helpers.dart';
|
import 'package:pshared/utils/payment/quote_helpers.dart';
|
||||||
import 'package:pshared/utils/exception.dart';
|
import 'package:pshared/utils/exception.dart';
|
||||||
|
|
||||||
|
|
||||||
class QuotationProvider extends ChangeNotifier {
|
class QuotationProvider extends ChangeNotifier {
|
||||||
static final _logger = Logger('provider.payment.quotation');
|
static final _logger = Logger('provider.payment.quotation');
|
||||||
Resource<PaymentQuote> _quotation = Resource(
|
Resource<PaymentQuote> _quotation = Resource(
|
||||||
|
|||||||
Reference in New Issue
Block a user