294 lines
13 KiB
Go
294 lines
13 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"
|
|
connectorv1 "github.com/tech/sendico/pkg/proto/connector/v1"
|
|
moneyv1 "github.com/tech/sendico/pkg/proto/common/money/v1"
|
|
mntxv1 "github.com/tech/sendico/pkg/proto/gateway/mntx/v1"
|
|
)
|
|
|
|
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()
|
|
if strings.TrimSpace(op.GetIdempotencyKey()) == "" {
|
|
return &connectorv1.SubmitOperationResponse{Receipt: &connectorv1.OperationReceipt{Error: connectorError(connectorv1.ErrorCode_INVALID_PARAMS, "submit_operation: idempotency_key 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
|
|
}
|
|
|
|
payoutID := strings.TrimSpace(reader.String("payout_id"))
|
|
if payoutID == "" {
|
|
payoutID = strings.TrimSpace(op.GetIdempotencyKey())
|
|
}
|
|
|
|
if strings.TrimSpace(reader.String("card_token")) != "" {
|
|
resp, err := s.CreateCardTokenPayout(ctx, buildCardTokenPayoutRequestFromParams(reader, payoutID, 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
|
|
}
|
|
resp, err := s.CreateCardPayout(ctx, buildCardPayoutRequestFromParams(reader, payoutID, 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
|
|
}
|
|
|
|
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")
|
|
}
|
|
resp, err := s.GetCardPayoutStatus(ctx, &mntxv1.GetCardPayoutStatusRequest{PayoutId: strings.TrimSpace(req.GetOperationId())})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &connectorv1.GetOperationResponse{Operation: payoutToOperation(resp.GetPayout())}, 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: "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 buildCardTokenPayoutRequestFromParams(reader params.Reader, payoutID string, amountMinor int64, currency string) *mntxv1.CardTokenPayoutRequest {
|
|
req := &mntxv1.CardTokenPayoutRequest{
|
|
PayoutId: payoutID,
|
|
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: reader.StringMap("metadata"),
|
|
}
|
|
return req
|
|
}
|
|
|
|
func buildCardPayoutRequestFromParams(reader params.Reader, payoutID string, amountMinor int64, currency string) *mntxv1.CardPayoutRequest {
|
|
return &mntxv1.CardPayoutRequest{
|
|
PayoutId: payoutID,
|
|
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: reader.StringMap("metadata"),
|
|
}
|
|
}
|
|
|
|
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_PENDING,
|
|
}
|
|
}
|
|
return &connectorv1.OperationReceipt{
|
|
OperationId: 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
|
|
}
|
|
return &connectorv1.Operation{
|
|
OperationId: 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()),
|
|
CreatedAt: state.GetCreatedAt(),
|
|
UpdatedAt: state.GetUpdatedAt(),
|
|
}
|
|
}
|
|
|
|
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_PROCESSED:
|
|
return connectorv1.OperationStatus_CONFIRMED
|
|
case mntxv1.PayoutStatus_PAYOUT_STATUS_FAILED:
|
|
return connectorv1.OperationStatus_FAILED
|
|
case mntxv1.PayoutStatus_PAYOUT_STATUS_PENDING:
|
|
return connectorv1.OperationStatus_PENDING
|
|
default:
|
|
return connectorv1.OperationStatus_PENDING
|
|
}
|
|
}
|
|
|
|
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
|
|
}
|
|
}
|