331 lines
12 KiB
Go
331 lines
12 KiB
Go
package client
|
|
|
|
import (
|
|
"context"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/shopspring/decimal"
|
|
"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"
|
|
"go.uber.org/zap"
|
|
"google.golang.org/grpc"
|
|
"google.golang.org/grpc/credentials/insecure"
|
|
"google.golang.org/protobuf/types/known/structpb"
|
|
)
|
|
|
|
// Client wraps the Monetix gateway gRPC API.
|
|
type Client interface {
|
|
CreateCardPayout(ctx context.Context, req *mntxv1.CardPayoutRequest) (*mntxv1.CardPayoutResponse, error)
|
|
CreateCardTokenPayout(ctx context.Context, req *mntxv1.CardTokenPayoutRequest) (*mntxv1.CardTokenPayoutResponse, error)
|
|
GetCardPayoutStatus(ctx context.Context, req *mntxv1.GetCardPayoutStatusRequest) (*mntxv1.GetCardPayoutStatusResponse, error)
|
|
ListGatewayInstances(ctx context.Context, req *mntxv1.ListGatewayInstancesRequest) (*mntxv1.ListGatewayInstancesResponse, error)
|
|
Close() error
|
|
}
|
|
|
|
type grpcConnectorClient interface {
|
|
SubmitOperation(ctx context.Context, in *connectorv1.SubmitOperationRequest, opts ...grpc.CallOption) (*connectorv1.SubmitOperationResponse, error)
|
|
GetOperation(ctx context.Context, in *connectorv1.GetOperationRequest, opts ...grpc.CallOption) (*connectorv1.GetOperationResponse, error)
|
|
}
|
|
|
|
type gatewayClient struct {
|
|
conn *grpc.ClientConn
|
|
client grpcConnectorClient
|
|
cfg Config
|
|
logger *zap.Logger
|
|
}
|
|
|
|
// New dials the Monetix gateway.
|
|
func New(ctx context.Context, cfg Config, opts ...grpc.DialOption) (Client, error) {
|
|
cfg.setDefaults()
|
|
if strings.TrimSpace(cfg.Address) == "" {
|
|
return nil, merrors.InvalidArgument("mntx: address is required")
|
|
}
|
|
dialCtx, cancel := context.WithTimeout(ctx, cfg.DialTimeout)
|
|
defer cancel()
|
|
|
|
dialOpts := make([]grpc.DialOption, 0, len(opts)+1)
|
|
dialOpts = append(dialOpts, grpc.WithTransportCredentials(insecure.NewCredentials()))
|
|
dialOpts = append(dialOpts, opts...)
|
|
|
|
conn, err := grpc.DialContext(dialCtx, cfg.Address, dialOpts...)
|
|
if err != nil {
|
|
return nil, merrors.Internal("mntx: dial failed: "+err.Error())
|
|
}
|
|
|
|
return &gatewayClient{
|
|
conn: conn,
|
|
client: connectorv1.NewConnectorServiceClient(conn),
|
|
cfg: cfg,
|
|
logger: cfg.Logger,
|
|
}, nil
|
|
}
|
|
|
|
func (g *gatewayClient) Close() error {
|
|
if g.conn != nil {
|
|
return g.conn.Close()
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (g *gatewayClient) callContext(ctx context.Context, method string) (context.Context, context.CancelFunc) {
|
|
if ctx == nil {
|
|
ctx = context.Background()
|
|
}
|
|
timeout := g.cfg.CallTimeout
|
|
if timeout <= 0 {
|
|
timeout = 5 * time.Second
|
|
}
|
|
if g.logger != nil {
|
|
fields := []zap.Field{
|
|
zap.String("method", method),
|
|
zap.Duration("timeout", timeout),
|
|
}
|
|
if deadline, ok := ctx.Deadline(); ok {
|
|
fields = append(fields, zap.Time("parent_deadline", deadline), zap.Duration("parent_deadline_in", time.Until(deadline)))
|
|
}
|
|
g.logger.Info("Mntx gateway client call timeout applied", fields...)
|
|
}
|
|
return context.WithTimeout(ctx, timeout)
|
|
}
|
|
|
|
func (g *gatewayClient) CreateCardPayout(ctx context.Context, req *mntxv1.CardPayoutRequest) (*mntxv1.CardPayoutResponse, error) {
|
|
ctx, cancel := g.callContext(ctx, "CreateCardPayout")
|
|
defer cancel()
|
|
operation, err := operationFromCardPayout(req)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
resp, err := g.client.SubmitOperation(ctx, &connectorv1.SubmitOperationRequest{Operation: operation})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if resp.GetReceipt() != nil && resp.GetReceipt().GetError() != nil {
|
|
return nil, connectorError(resp.GetReceipt().GetError())
|
|
}
|
|
return &mntxv1.CardPayoutResponse{Payout: payoutFromReceipt(req.GetPayoutId(), resp.GetReceipt())}, nil
|
|
}
|
|
|
|
func (g *gatewayClient) CreateCardTokenPayout(ctx context.Context, req *mntxv1.CardTokenPayoutRequest) (*mntxv1.CardTokenPayoutResponse, error) {
|
|
ctx, cancel := g.callContext(ctx, "CreateCardTokenPayout")
|
|
defer cancel()
|
|
operation, err := operationFromTokenPayout(req)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
resp, err := g.client.SubmitOperation(ctx, &connectorv1.SubmitOperationRequest{Operation: operation})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if resp.GetReceipt() != nil && resp.GetReceipt().GetError() != nil {
|
|
return nil, connectorError(resp.GetReceipt().GetError())
|
|
}
|
|
return &mntxv1.CardTokenPayoutResponse{Payout: payoutFromReceipt(req.GetPayoutId(), resp.GetReceipt())}, nil
|
|
}
|
|
|
|
func (g *gatewayClient) GetCardPayoutStatus(ctx context.Context, req *mntxv1.GetCardPayoutStatusRequest) (*mntxv1.GetCardPayoutStatusResponse, error) {
|
|
ctx, cancel := g.callContext(ctx, "GetCardPayoutStatus")
|
|
defer cancel()
|
|
if req == nil || strings.TrimSpace(req.GetPayoutId()) == "" {
|
|
return nil, merrors.InvalidArgument("mntx: payout_id is required")
|
|
}
|
|
resp, err := g.client.GetOperation(ctx, &connectorv1.GetOperationRequest{OperationId: strings.TrimSpace(req.GetPayoutId())})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &mntxv1.GetCardPayoutStatusResponse{Payout: payoutFromOperation(resp.GetOperation())}, nil
|
|
}
|
|
|
|
func (g *gatewayClient) ListGatewayInstances(ctx context.Context, req *mntxv1.ListGatewayInstancesRequest) (*mntxv1.ListGatewayInstancesResponse, error) {
|
|
return nil, merrors.NotImplemented("mntx: ListGatewayInstances not supported via connector")
|
|
}
|
|
|
|
func operationFromCardPayout(req *mntxv1.CardPayoutRequest) (*connectorv1.Operation, error) {
|
|
if req == nil {
|
|
return nil, merrors.InvalidArgument("mntx: request is required")
|
|
}
|
|
params := payoutParamsFromCard(req)
|
|
money := moneyFromMinor(req.GetAmountMinor(), req.GetCurrency())
|
|
return &connectorv1.Operation{
|
|
Type: connectorv1.OperationType_PAYOUT,
|
|
IdempotencyKey: strings.TrimSpace(req.GetPayoutId()),
|
|
Money: money,
|
|
Params: structFromMap(params),
|
|
}, nil
|
|
}
|
|
|
|
func operationFromTokenPayout(req *mntxv1.CardTokenPayoutRequest) (*connectorv1.Operation, error) {
|
|
if req == nil {
|
|
return nil, merrors.InvalidArgument("mntx: request is required")
|
|
}
|
|
params := payoutParamsFromToken(req)
|
|
money := moneyFromMinor(req.GetAmountMinor(), req.GetCurrency())
|
|
return &connectorv1.Operation{
|
|
Type: connectorv1.OperationType_PAYOUT,
|
|
IdempotencyKey: strings.TrimSpace(req.GetPayoutId()),
|
|
Money: money,
|
|
Params: structFromMap(params),
|
|
}, nil
|
|
}
|
|
|
|
func payoutParamsFromCard(req *mntxv1.CardPayoutRequest) map[string]interface{} {
|
|
params := map[string]interface{}{
|
|
"payout_id": strings.TrimSpace(req.GetPayoutId()),
|
|
"project_id": req.GetProjectId(),
|
|
"customer_id": strings.TrimSpace(req.GetCustomerId()),
|
|
"customer_first_name": strings.TrimSpace(req.GetCustomerFirstName()),
|
|
"customer_middle_name": strings.TrimSpace(req.GetCustomerMiddleName()),
|
|
"customer_last_name": strings.TrimSpace(req.GetCustomerLastName()),
|
|
"customer_ip": strings.TrimSpace(req.GetCustomerIp()),
|
|
"customer_zip": strings.TrimSpace(req.GetCustomerZip()),
|
|
"customer_country": strings.TrimSpace(req.GetCustomerCountry()),
|
|
"customer_state": strings.TrimSpace(req.GetCustomerState()),
|
|
"customer_city": strings.TrimSpace(req.GetCustomerCity()),
|
|
"customer_address": strings.TrimSpace(req.GetCustomerAddress()),
|
|
"amount_minor": req.GetAmountMinor(),
|
|
"currency": strings.TrimSpace(req.GetCurrency()),
|
|
"card_pan": strings.TrimSpace(req.GetCardPan()),
|
|
"card_exp_year": req.GetCardExpYear(),
|
|
"card_exp_month": req.GetCardExpMonth(),
|
|
"card_holder": strings.TrimSpace(req.GetCardHolder()),
|
|
}
|
|
if len(req.GetMetadata()) > 0 {
|
|
params["metadata"] = mapStringToInterface(req.GetMetadata())
|
|
}
|
|
return params
|
|
}
|
|
|
|
func payoutParamsFromToken(req *mntxv1.CardTokenPayoutRequest) map[string]interface{} {
|
|
params := map[string]interface{}{
|
|
"payout_id": strings.TrimSpace(req.GetPayoutId()),
|
|
"project_id": req.GetProjectId(),
|
|
"customer_id": strings.TrimSpace(req.GetCustomerId()),
|
|
"customer_first_name": strings.TrimSpace(req.GetCustomerFirstName()),
|
|
"customer_middle_name": strings.TrimSpace(req.GetCustomerMiddleName()),
|
|
"customer_last_name": strings.TrimSpace(req.GetCustomerLastName()),
|
|
"customer_ip": strings.TrimSpace(req.GetCustomerIp()),
|
|
"customer_zip": strings.TrimSpace(req.GetCustomerZip()),
|
|
"customer_country": strings.TrimSpace(req.GetCustomerCountry()),
|
|
"customer_state": strings.TrimSpace(req.GetCustomerState()),
|
|
"customer_city": strings.TrimSpace(req.GetCustomerCity()),
|
|
"customer_address": strings.TrimSpace(req.GetCustomerAddress()),
|
|
"amount_minor": req.GetAmountMinor(),
|
|
"currency": strings.TrimSpace(req.GetCurrency()),
|
|
"card_token": strings.TrimSpace(req.GetCardToken()),
|
|
"card_holder": strings.TrimSpace(req.GetCardHolder()),
|
|
"masked_pan": strings.TrimSpace(req.GetMaskedPan()),
|
|
}
|
|
if len(req.GetMetadata()) > 0 {
|
|
params["metadata"] = mapStringToInterface(req.GetMetadata())
|
|
}
|
|
return params
|
|
}
|
|
|
|
func moneyFromMinor(amount int64, currency string) *moneyv1.Money {
|
|
if amount <= 0 {
|
|
return nil
|
|
}
|
|
dec := decimal.NewFromInt(amount).Div(decimal.NewFromInt(100))
|
|
return &moneyv1.Money{
|
|
Amount: dec.StringFixed(2),
|
|
Currency: strings.ToUpper(strings.TrimSpace(currency)),
|
|
}
|
|
}
|
|
|
|
func payoutFromReceipt(payoutID string, receipt *connectorv1.OperationReceipt) *mntxv1.CardPayoutState {
|
|
state := &mntxv1.CardPayoutState{PayoutId: strings.TrimSpace(payoutID)}
|
|
if receipt == nil {
|
|
return state
|
|
}
|
|
state.Status = payoutStatusFromOperation(receipt.GetStatus())
|
|
state.ProviderPaymentId = strings.TrimSpace(receipt.GetProviderRef())
|
|
return state
|
|
}
|
|
|
|
func payoutFromOperation(op *connectorv1.Operation) *mntxv1.CardPayoutState {
|
|
if op == nil {
|
|
return nil
|
|
}
|
|
state := &mntxv1.CardPayoutState{
|
|
PayoutId: strings.TrimSpace(op.GetOperationId()),
|
|
Status: payoutStatusFromOperation(op.GetStatus()),
|
|
ProviderPaymentId: strings.TrimSpace(op.GetProviderRef()),
|
|
}
|
|
if money := op.GetMoney(); money != nil {
|
|
state.Currency = strings.TrimSpace(money.GetCurrency())
|
|
state.AmountMinor = minorFromMoney(money)
|
|
}
|
|
return state
|
|
}
|
|
|
|
func minorFromMoney(m *moneyv1.Money) int64 {
|
|
if m == nil {
|
|
return 0
|
|
}
|
|
amount := strings.TrimSpace(m.GetAmount())
|
|
if amount == "" {
|
|
return 0
|
|
}
|
|
dec, err := decimal.NewFromString(amount)
|
|
if err != nil {
|
|
return 0
|
|
}
|
|
return dec.Mul(decimal.NewFromInt(100)).IntPart()
|
|
}
|
|
|
|
func payoutStatusFromOperation(status connectorv1.OperationStatus) mntxv1.PayoutStatus {
|
|
switch status {
|
|
case connectorv1.OperationStatus_CONFIRMED:
|
|
return mntxv1.PayoutStatus_PAYOUT_STATUS_PROCESSED
|
|
case connectorv1.OperationStatus_FAILED:
|
|
return mntxv1.PayoutStatus_PAYOUT_STATUS_FAILED
|
|
case connectorv1.OperationStatus_PENDING, connectorv1.OperationStatus_SUBMITTED:
|
|
return mntxv1.PayoutStatus_PAYOUT_STATUS_PENDING
|
|
default:
|
|
return mntxv1.PayoutStatus_PAYOUT_STATUS_UNSPECIFIED
|
|
}
|
|
}
|
|
|
|
func connectorError(err *connectorv1.ConnectorError) error {
|
|
if err == nil {
|
|
return nil
|
|
}
|
|
msg := strings.TrimSpace(err.GetMessage())
|
|
switch err.GetCode() {
|
|
case connectorv1.ErrorCode_INVALID_PARAMS:
|
|
return merrors.InvalidArgument(msg)
|
|
case connectorv1.ErrorCode_NOT_FOUND:
|
|
return merrors.NoData(msg)
|
|
case connectorv1.ErrorCode_UNSUPPORTED_OPERATION, connectorv1.ErrorCode_UNSUPPORTED_ACCOUNT_KIND:
|
|
return merrors.NotImplemented(msg)
|
|
case connectorv1.ErrorCode_RATE_LIMITED, connectorv1.ErrorCode_TEMPORARY_UNAVAILABLE:
|
|
return merrors.Internal(msg)
|
|
default:
|
|
return merrors.Internal(msg)
|
|
}
|
|
}
|
|
|
|
func structFromMap(data map[string]interface{}) *structpb.Struct {
|
|
if len(data) == 0 {
|
|
return nil
|
|
}
|
|
result, err := structpb.NewStruct(data)
|
|
if err != nil {
|
|
return nil
|
|
}
|
|
return result
|
|
}
|
|
|
|
func mapStringToInterface(input map[string]string) map[string]interface{} {
|
|
if len(input) == 0 {
|
|
return nil
|
|
}
|
|
out := make(map[string]interface{}, len(input))
|
|
for k, v := range input {
|
|
out[k] = v
|
|
}
|
|
return out
|
|
}
|