interface refactoring
This commit is contained in:
294
api/gateway/mntx/internal/service/gateway/connector.go
Normal file
294
api/gateway/mntx/internal/service/gateway/connector.go
Normal file
@@ -0,0 +1,294 @@
|
||||
package gateway
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"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, buildCardTokenPayoutRequest(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, buildCardPayoutRequest(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, "", fmt.Errorf("payout: operation is required")
|
||||
}
|
||||
currency := currencyFromOperation(op)
|
||||
if currency == "" {
|
||||
return 0, "", fmt.Errorf("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, "", fmt.Errorf("payout: money is required")
|
||||
}
|
||||
amount := strings.TrimSpace(money.GetAmount())
|
||||
if amount == "" {
|
||||
return 0, "", fmt.Errorf("payout: amount is required")
|
||||
}
|
||||
dec, err := decimal.NewFromString(amount)
|
||||
if err != nil {
|
||||
return 0, "", fmt.Errorf("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 buildCardTokenPayoutRequest(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 buildCardPayoutRequest(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
|
||||
}
|
||||
}
|
||||
@@ -15,7 +15,7 @@ import (
|
||||
"github.com/tech/sendico/pkg/mlogger"
|
||||
"github.com/tech/sendico/pkg/mservice"
|
||||
gatewayv1 "github.com/tech/sendico/pkg/proto/common/gateway/v1"
|
||||
unifiedv1 "github.com/tech/sendico/pkg/proto/gateway/unified/v1"
|
||||
connectorv1 "github.com/tech/sendico/pkg/proto/connector/v1"
|
||||
"go.uber.org/zap"
|
||||
"google.golang.org/grpc"
|
||||
)
|
||||
@@ -31,7 +31,7 @@ type Service struct {
|
||||
gatewayDescriptor *gatewayv1.GatewayInstanceDescriptor
|
||||
announcer *discovery.Announcer
|
||||
|
||||
unifiedv1.UnimplementedUnifiedGatewayServiceServer
|
||||
connectorv1.UnimplementedConnectorServiceServer
|
||||
}
|
||||
|
||||
type payoutFailure interface {
|
||||
@@ -96,7 +96,7 @@ func NewService(logger mlogger.Logger, opts ...Option) *Service {
|
||||
// Register wires the service onto the provided gRPC router.
|
||||
func (s *Service) Register(router routers.GRPC) error {
|
||||
return router.Register(func(reg grpc.ServiceRegistrar) {
|
||||
unifiedv1.RegisterUnifiedGatewayServiceServer(reg, s)
|
||||
connectorv1.RegisterConnectorServiceServer(reg, s)
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user