389 lines
16 KiB
Go
389 lines
16 KiB
Go
package gateway
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"strings"
|
|
|
|
"github.com/shopspring/decimal"
|
|
"github.com/tech/sendico/gateway/mntx/internal/appversion"
|
|
"github.com/tech/sendico/pkg/connector/params"
|
|
"github.com/tech/sendico/pkg/merrors"
|
|
moneyv1 "github.com/tech/sendico/pkg/proto/common/money/v1"
|
|
connectorv1 "github.com/tech/sendico/pkg/proto/connector/v1"
|
|
mntxv1 "github.com/tech/sendico/pkg/proto/gateway/mntx/v1"
|
|
"google.golang.org/protobuf/types/known/structpb"
|
|
)
|
|
|
|
const mntxConnectorID = "mntx"
|
|
|
|
func (s *Service) GetCapabilities(_ context.Context, _ *connectorv1.GetCapabilitiesRequest) (*connectorv1.GetCapabilitiesResponse, error) {
|
|
return &connectorv1.GetCapabilitiesResponse{
|
|
Capabilities: &connectorv1.ConnectorCapabilities{
|
|
ConnectorType: mntxConnectorID,
|
|
Version: appversion.Create().Short(),
|
|
SupportedAccountKinds: nil,
|
|
SupportedOperationTypes: []connectorv1.OperationType{connectorv1.OperationType_PAYOUT},
|
|
OperationParams: mntxOperationParams(),
|
|
},
|
|
}, nil
|
|
}
|
|
|
|
func (s *Service) OpenAccount(_ context.Context, _ *connectorv1.OpenAccountRequest) (*connectorv1.OpenAccountResponse, error) {
|
|
return &connectorv1.OpenAccountResponse{Error: connectorError(connectorv1.ErrorCode_UNSUPPORTED_ACCOUNT_KIND, "open_account: unsupported", nil, "")}, nil
|
|
}
|
|
|
|
func (s *Service) GetAccount(_ context.Context, _ *connectorv1.GetAccountRequest) (*connectorv1.GetAccountResponse, error) {
|
|
return nil, merrors.NotImplemented("get_account: unsupported")
|
|
}
|
|
|
|
func (s *Service) ListAccounts(_ context.Context, _ *connectorv1.ListAccountsRequest) (*connectorv1.ListAccountsResponse, error) {
|
|
return nil, merrors.NotImplemented("list_accounts: unsupported")
|
|
}
|
|
|
|
func (s *Service) GetBalance(_ context.Context, _ *connectorv1.GetBalanceRequest) (*connectorv1.GetBalanceResponse, error) {
|
|
return nil, merrors.NotImplemented("get_balance: unsupported")
|
|
}
|
|
|
|
func (s *Service) SubmitOperation(ctx context.Context, req *connectorv1.SubmitOperationRequest) (*connectorv1.SubmitOperationResponse, error) {
|
|
if req == nil || req.GetOperation() == nil {
|
|
return &connectorv1.SubmitOperationResponse{Receipt: &connectorv1.OperationReceipt{Error: connectorError(connectorv1.ErrorCode_INVALID_PARAMS, "submit_operation: operation is required", nil, "")}}, nil
|
|
}
|
|
op := req.GetOperation()
|
|
idempotencyKey := strings.TrimSpace(op.GetIdempotencyKey())
|
|
if idempotencyKey == "" {
|
|
return &connectorv1.SubmitOperationResponse{Receipt: &connectorv1.OperationReceipt{Error: connectorError(connectorv1.ErrorCode_INVALID_PARAMS, "submit_operation: idempotency_key is required", op, "")}}, nil
|
|
}
|
|
operationRef := strings.TrimSpace(op.GetOperationRef())
|
|
if operationRef == "" {
|
|
return &connectorv1.SubmitOperationResponse{Receipt: &connectorv1.OperationReceipt{Error: connectorError(connectorv1.ErrorCode_INVALID_PARAMS, "submit_operation: operation_ref is required", op, "")}}, nil
|
|
}
|
|
intentRef := strings.TrimSpace(op.GetIntentRef())
|
|
if intentRef == "" {
|
|
return &connectorv1.SubmitOperationResponse{Receipt: &connectorv1.OperationReceipt{Error: connectorError(connectorv1.ErrorCode_INVALID_PARAMS, "submit_operation: intent_ref is required", op, "")}}, nil
|
|
}
|
|
if op.GetType() != connectorv1.OperationType_PAYOUT {
|
|
return &connectorv1.SubmitOperationResponse{Receipt: &connectorv1.OperationReceipt{Error: connectorError(connectorv1.ErrorCode_UNSUPPORTED_OPERATION, "submit_operation: unsupported operation type", op, "")}}, nil
|
|
}
|
|
reader := params.New(op.GetParams())
|
|
amountMinor, currency, err := payoutAmount(op, reader)
|
|
if err != 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 := operationIDForRequest(operationRef)
|
|
|
|
if strings.TrimSpace(reader.String("card_token")) != "" {
|
|
resp, err := s.CreateCardTokenPayout(ctx, buildCardTokenPayoutRequestFromParams(reader, payoutID, parentPaymentRef, idempotencyKey, operationRef, intentRef, amountMinor, currency))
|
|
if err != nil {
|
|
return &connectorv1.SubmitOperationResponse{Receipt: &connectorv1.OperationReceipt{Error: connectorError(mapErrorCode(err), err.Error(), op, "")}}, nil
|
|
}
|
|
return &connectorv1.SubmitOperationResponse{Receipt: payoutReceipt(resp.GetPayout())}, nil
|
|
}
|
|
cr := buildCardPayoutRequestFromParams(reader, payoutID, parentPaymentRef, idempotencyKey, operationRef, intentRef, amountMinor, currency)
|
|
resp, err := s.CreateCardPayout(ctx, cr)
|
|
if err != nil {
|
|
return &connectorv1.SubmitOperationResponse{Receipt: &connectorv1.OperationReceipt{Error: connectorError(mapErrorCode(err), err.Error(), op, "")}}, nil
|
|
}
|
|
return &connectorv1.SubmitOperationResponse{Receipt: payoutReceipt(resp.GetPayout())}, nil
|
|
}
|
|
|
|
func (s *Service) GetOperation(ctx context.Context, req *connectorv1.GetOperationRequest) (*connectorv1.GetOperationResponse, error) {
|
|
if req == nil || strings.TrimSpace(req.GetOperationId()) == "" {
|
|
return nil, merrors.InvalidArgument("get_operation: operation_id is required")
|
|
}
|
|
|
|
operationRef := strings.TrimSpace(req.GetOperationId())
|
|
if s.storage == nil || s.storage.Payouts() == nil {
|
|
return nil, merrors.Internal("get_operation: storage is not configured")
|
|
}
|
|
|
|
payout, err := s.storage.Payouts().FindByOperationRef(ctx, operationRef)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if payout == nil {
|
|
return nil, merrors.NoData("payout not found")
|
|
}
|
|
|
|
return &connectorv1.GetOperationResponse{Operation: payoutToOperation(StateToProto(payout))}, nil
|
|
}
|
|
|
|
func (s *Service) ListOperations(_ context.Context, _ *connectorv1.ListOperationsRequest) (*connectorv1.ListOperationsResponse, error) {
|
|
return nil, merrors.NotImplemented("list_operations: unsupported")
|
|
}
|
|
|
|
func mntxOperationParams() []*connectorv1.OperationParamSpec {
|
|
return []*connectorv1.OperationParamSpec{
|
|
{OperationType: connectorv1.OperationType_PAYOUT, Params: []*connectorv1.ParamSpec{
|
|
{Key: "customer_id", Type: connectorv1.ParamType_STRING, Required: true},
|
|
{Key: "customer_first_name", Type: connectorv1.ParamType_STRING, Required: true},
|
|
{Key: "customer_last_name", Type: connectorv1.ParamType_STRING, Required: true},
|
|
{Key: "customer_ip", Type: connectorv1.ParamType_STRING, Required: true},
|
|
{Key: "card_token", Type: connectorv1.ParamType_STRING, Required: false},
|
|
{Key: "card_pan", Type: connectorv1.ParamType_STRING, Required: false},
|
|
{Key: "card_exp_year", Type: connectorv1.ParamType_INT, Required: false},
|
|
{Key: "card_exp_month", Type: connectorv1.ParamType_INT, Required: false},
|
|
{Key: "card_holder", Type: connectorv1.ParamType_STRING, Required: false},
|
|
{Key: "amount_minor", Type: connectorv1.ParamType_INT, Required: false},
|
|
{Key: "project_id", Type: connectorv1.ParamType_INT, Required: false},
|
|
{Key: "parent_payment_ref", Type: connectorv1.ParamType_STRING, Required: true},
|
|
{Key: "customer_middle_name", Type: connectorv1.ParamType_STRING, Required: false},
|
|
{Key: "customer_country", Type: connectorv1.ParamType_STRING, Required: false},
|
|
{Key: "customer_state", Type: connectorv1.ParamType_STRING, Required: false},
|
|
{Key: "customer_city", Type: connectorv1.ParamType_STRING, Required: false},
|
|
{Key: "customer_address", Type: connectorv1.ParamType_STRING, Required: false},
|
|
{Key: "customer_zip", Type: connectorv1.ParamType_STRING, Required: false},
|
|
{Key: "masked_pan", Type: connectorv1.ParamType_STRING, Required: false},
|
|
{Key: "metadata", Type: connectorv1.ParamType_JSON, Required: false},
|
|
}},
|
|
}
|
|
}
|
|
|
|
func payoutAmount(op *connectorv1.Operation, reader params.Reader) (int64, string, error) {
|
|
if op == nil {
|
|
return 0, "", merrors.InvalidArgument("payout: operation is required")
|
|
}
|
|
currency := currencyFromOperation(op)
|
|
if currency == "" {
|
|
return 0, "", merrors.InvalidArgument("payout: currency is required")
|
|
}
|
|
if minor, ok := reader.Int64("amount_minor"); ok && minor > 0 {
|
|
return minor, currency, nil
|
|
}
|
|
money := op.GetMoney()
|
|
if money == nil {
|
|
return 0, "", merrors.InvalidArgument("payout: money is required")
|
|
}
|
|
amount := strings.TrimSpace(money.GetAmount())
|
|
if amount == "" {
|
|
return 0, "", merrors.InvalidArgument("payout: amount is required")
|
|
}
|
|
dec, err := decimal.NewFromString(amount)
|
|
if err != nil {
|
|
return 0, "", merrors.InvalidArgument("payout: invalid amount")
|
|
}
|
|
minor := dec.Mul(decimal.NewFromInt(100)).IntPart()
|
|
return minor, currency, nil
|
|
}
|
|
|
|
func currencyFromOperation(op *connectorv1.Operation) string {
|
|
if op == nil || op.GetMoney() == nil {
|
|
return ""
|
|
}
|
|
currency := strings.TrimSpace(op.GetMoney().GetCurrency())
|
|
if idx := strings.Index(currency, "-"); idx > 0 {
|
|
currency = currency[:idx]
|
|
}
|
|
return strings.ToUpper(currency)
|
|
}
|
|
|
|
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{
|
|
PayoutId: payoutID,
|
|
ParentPaymentRef: strings.TrimSpace(parentPaymentRef),
|
|
ProjectId: readerInt64(reader, "project_id"),
|
|
CustomerId: strings.TrimSpace(reader.String("customer_id")),
|
|
CustomerFirstName: strings.TrimSpace(reader.String("customer_first_name")),
|
|
CustomerMiddleName: strings.TrimSpace(reader.String("customer_middle_name")),
|
|
CustomerLastName: strings.TrimSpace(reader.String("customer_last_name")),
|
|
CustomerIp: strings.TrimSpace(reader.String("customer_ip")),
|
|
CustomerZip: strings.TrimSpace(reader.String("customer_zip")),
|
|
CustomerCountry: strings.TrimSpace(reader.String("customer_country")),
|
|
CustomerState: strings.TrimSpace(reader.String("customer_state")),
|
|
CustomerCity: strings.TrimSpace(reader.String("customer_city")),
|
|
CustomerAddress: strings.TrimSpace(reader.String("customer_address")),
|
|
AmountMinor: amountMinor,
|
|
Currency: currency,
|
|
CardToken: strings.TrimSpace(reader.String("card_token")),
|
|
CardHolder: strings.TrimSpace(reader.String("card_holder")),
|
|
MaskedPan: strings.TrimSpace(reader.String("masked_pan")),
|
|
Metadata: metadataFromReader(reader),
|
|
OperationRef: operationRef,
|
|
IdempotencyKey: strings.TrimSpace(idempotencyKey),
|
|
IntentRef: strings.TrimSpace(intentRef),
|
|
}
|
|
return req
|
|
}
|
|
|
|
func buildCardPayoutRequestFromParams(reader params.Reader,
|
|
payoutID, parentPaymentRef, 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,
|
|
ParentPaymentRef: strings.TrimSpace(parentPaymentRef),
|
|
ProjectId: readerInt64(reader, "project_id"),
|
|
CustomerId: strings.TrimSpace(reader.String("customer_id")),
|
|
CustomerFirstName: strings.TrimSpace(reader.String("customer_first_name")),
|
|
CustomerMiddleName: strings.TrimSpace(reader.String("customer_middle_name")),
|
|
CustomerLastName: strings.TrimSpace(reader.String("customer_last_name")),
|
|
CustomerIp: strings.TrimSpace(reader.String("customer_ip")),
|
|
CustomerZip: strings.TrimSpace(reader.String("customer_zip")),
|
|
CustomerCountry: strings.TrimSpace(reader.String("customer_country")),
|
|
CustomerState: strings.TrimSpace(reader.String("customer_state")),
|
|
CustomerCity: strings.TrimSpace(reader.String("customer_city")),
|
|
CustomerAddress: strings.TrimSpace(reader.String("customer_address")),
|
|
AmountMinor: amountMinor,
|
|
Currency: currency,
|
|
CardPan: strings.TrimSpace(reader.String("card_pan")),
|
|
CardExpYear: uint32(readerInt64(reader, "card_exp_year")),
|
|
CardExpMonth: uint32(readerInt64(reader, "card_exp_month")),
|
|
CardHolder: strings.TrimSpace(reader.String("card_holder")),
|
|
Metadata: metadataFromReader(reader),
|
|
OperationRef: operationRef,
|
|
IdempotencyKey: strings.TrimSpace(idempotencyKey),
|
|
IntentRef: strings.TrimSpace(intentRef),
|
|
}
|
|
}
|
|
|
|
func readerInt64(reader params.Reader, key string) int64 {
|
|
if v, ok := reader.Int64(key); ok {
|
|
return v
|
|
}
|
|
return 0
|
|
}
|
|
|
|
func payoutReceipt(state *mntxv1.CardPayoutState) *connectorv1.OperationReceipt {
|
|
if state == nil {
|
|
return &connectorv1.OperationReceipt{
|
|
Status: connectorv1.OperationStatus_OPERATION_PROCESSING,
|
|
}
|
|
}
|
|
return &connectorv1.OperationReceipt{
|
|
OperationId: firstNonEmpty(strings.TrimSpace(state.GetOperationRef()), strings.TrimSpace(state.GetPayoutId())),
|
|
Status: payoutStatusToOperation(state.GetStatus()),
|
|
ProviderRef: strings.TrimSpace(state.GetProviderPaymentId()),
|
|
}
|
|
}
|
|
|
|
func payoutToOperation(state *mntxv1.CardPayoutState) *connectorv1.Operation {
|
|
if state == nil {
|
|
return nil
|
|
}
|
|
op := &connectorv1.Operation{
|
|
OperationId: firstNonEmpty(strings.TrimSpace(state.GetOperationRef()), strings.TrimSpace(state.GetPayoutId())),
|
|
Type: connectorv1.OperationType_PAYOUT,
|
|
Status: payoutStatusToOperation(state.GetStatus()),
|
|
Money: &moneyv1.Money{
|
|
Amount: minorToDecimal(state.GetAmountMinor()),
|
|
Currency: strings.ToUpper(strings.TrimSpace(state.GetCurrency())),
|
|
},
|
|
ProviderRef: strings.TrimSpace(state.GetProviderPaymentId()),
|
|
IntentRef: strings.TrimSpace(state.GetIntentRef()),
|
|
OperationRef: strings.TrimSpace(state.GetOperationRef()),
|
|
CreatedAt: state.GetCreatedAt(),
|
|
UpdatedAt: state.GetUpdatedAt(),
|
|
}
|
|
|
|
params := map[string]interface{}{}
|
|
if paymentRef := strings.TrimSpace(state.GetParentPaymentRef()); paymentRef != "" {
|
|
params["payment_ref"] = paymentRef
|
|
params["parent_payment_ref"] = paymentRef
|
|
}
|
|
if providerCode := strings.TrimSpace(state.GetProviderCode()); providerCode != "" {
|
|
params["provider_code"] = providerCode
|
|
}
|
|
if providerMessage := strings.TrimSpace(state.GetProviderMessage()); providerMessage != "" {
|
|
params["provider_message"] = providerMessage
|
|
params["failure_reason"] = providerMessage
|
|
}
|
|
if len(params) > 0 {
|
|
op.Params = structFromMap(params)
|
|
}
|
|
|
|
return op
|
|
}
|
|
|
|
func minorToDecimal(amount int64) string {
|
|
dec := decimal.NewFromInt(amount).Div(decimal.NewFromInt(100))
|
|
return dec.StringFixed(2)
|
|
}
|
|
|
|
func payoutStatusToOperation(status mntxv1.PayoutStatus) connectorv1.OperationStatus {
|
|
switch status {
|
|
|
|
case mntxv1.PayoutStatus_PAYOUT_STATUS_CREATED:
|
|
return connectorv1.OperationStatus_OPERATION_CREATED
|
|
|
|
case mntxv1.PayoutStatus_PAYOUT_STATUS_WAITING:
|
|
return connectorv1.OperationStatus_OPERATION_WAITING
|
|
|
|
case mntxv1.PayoutStatus_PAYOUT_STATUS_SUCCESS:
|
|
return connectorv1.OperationStatus_OPERATION_SUCCESS
|
|
|
|
case mntxv1.PayoutStatus_PAYOUT_STATUS_FAILED:
|
|
return connectorv1.OperationStatus_OPERATION_FAILED
|
|
|
|
case mntxv1.PayoutStatus_PAYOUT_STATUS_CANCELLED:
|
|
return connectorv1.OperationStatus_OPERATION_CANCELLED
|
|
|
|
default:
|
|
return connectorv1.OperationStatus_OPERATION_STATUS_UNSPECIFIED
|
|
}
|
|
}
|
|
|
|
func structFromMap(values map[string]interface{}) *structpb.Struct {
|
|
if len(values) == 0 {
|
|
return nil
|
|
}
|
|
result, err := structpb.NewStruct(values)
|
|
if err != nil {
|
|
return nil
|
|
}
|
|
return result
|
|
}
|
|
|
|
func connectorError(code connectorv1.ErrorCode, message string, op *connectorv1.Operation, accountID string) *connectorv1.ConnectorError {
|
|
err := &connectorv1.ConnectorError{
|
|
Code: code,
|
|
Message: strings.TrimSpace(message),
|
|
AccountId: strings.TrimSpace(accountID),
|
|
}
|
|
if op != nil {
|
|
err.CorrelationId = strings.TrimSpace(op.GetCorrelationId())
|
|
err.ParentIntentId = strings.TrimSpace(op.GetParentIntentId())
|
|
err.OperationId = strings.TrimSpace(op.GetOperationId())
|
|
}
|
|
return err
|
|
}
|
|
|
|
func mapErrorCode(err error) connectorv1.ErrorCode {
|
|
switch {
|
|
case errors.Is(err, merrors.ErrInvalidArg):
|
|
return connectorv1.ErrorCode_INVALID_PARAMS
|
|
case errors.Is(err, merrors.ErrNoData):
|
|
return connectorv1.ErrorCode_NOT_FOUND
|
|
case errors.Is(err, merrors.ErrNotImplemented):
|
|
return connectorv1.ErrorCode_UNSUPPORTED_OPERATION
|
|
case errors.Is(err, merrors.ErrInternal):
|
|
return connectorv1.ErrorCode_TEMPORARY_UNAVAILABLE
|
|
default:
|
|
return connectorv1.ErrorCode_PROVIDER_ERROR
|
|
}
|
|
}
|