monetix gateway
This commit is contained in:
311
api/gateway/mntx/internal/service/gateway/card_processor.go
Normal file
311
api/gateway/mntx/internal/service/gateway/card_processor.go
Normal file
@@ -0,0 +1,311 @@
|
||||
package gateway
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/tech/sendico/gateway/mntx/internal/service/monetix"
|
||||
clockpkg "github.com/tech/sendico/pkg/clock"
|
||||
"github.com/tech/sendico/pkg/merrors"
|
||||
msg "github.com/tech/sendico/pkg/messaging"
|
||||
messaging "github.com/tech/sendico/pkg/messaging/envelope"
|
||||
"github.com/tech/sendico/pkg/mlogger"
|
||||
"github.com/tech/sendico/pkg/model"
|
||||
nm "github.com/tech/sendico/pkg/model/notification"
|
||||
"github.com/tech/sendico/pkg/mservice"
|
||||
mntxv1 "github.com/tech/sendico/pkg/proto/gateway/mntx/v1"
|
||||
"go.uber.org/zap"
|
||||
"google.golang.org/protobuf/encoding/protojson"
|
||||
"google.golang.org/protobuf/types/known/timestamppb"
|
||||
)
|
||||
|
||||
type cardPayoutProcessor struct {
|
||||
logger mlogger.Logger
|
||||
config monetix.Config
|
||||
clock clockpkg.Clock
|
||||
store *cardPayoutStore
|
||||
httpClient *http.Client
|
||||
producer msg.Producer
|
||||
}
|
||||
|
||||
func newCardPayoutProcessor(logger mlogger.Logger, cfg monetix.Config, clock clockpkg.Clock, store *cardPayoutStore, client *http.Client, producer msg.Producer) *cardPayoutProcessor {
|
||||
return &cardPayoutProcessor{
|
||||
logger: logger.Named("card_payout_processor"),
|
||||
config: cfg,
|
||||
clock: clock,
|
||||
store: store,
|
||||
httpClient: client,
|
||||
producer: producer,
|
||||
}
|
||||
}
|
||||
|
||||
func (p *cardPayoutProcessor) Submit(ctx context.Context, req *mntxv1.CardPayoutRequest) (*mntxv1.CardPayoutResponse, error) {
|
||||
if p == nil {
|
||||
return nil, merrors.Internal("card payout processor not initialised")
|
||||
}
|
||||
if strings.TrimSpace(p.config.BaseURL) == "" || strings.TrimSpace(p.config.SecretKey) == "" {
|
||||
return nil, merrors.Internal("monetix configuration is incomplete")
|
||||
}
|
||||
|
||||
req = sanitizeCardPayoutRequest(req)
|
||||
if err := validateCardPayoutRequest(req, p.config); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
projectID := req.GetProjectId()
|
||||
if projectID == 0 {
|
||||
projectID = p.config.ProjectID
|
||||
}
|
||||
if projectID == 0 {
|
||||
return nil, merrors.Internal("monetix project_id is not configured")
|
||||
}
|
||||
|
||||
now := timestamppb.New(p.clock.Now())
|
||||
state := &mntxv1.CardPayoutState{
|
||||
PayoutId: req.GetPayoutId(),
|
||||
ProjectId: projectID,
|
||||
CustomerId: req.GetCustomerId(),
|
||||
AmountMinor: req.GetAmountMinor(),
|
||||
Currency: strings.ToUpper(strings.TrimSpace(req.GetCurrency())),
|
||||
Status: mntxv1.PayoutStatus_PAYOUT_STATUS_PENDING,
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
}
|
||||
|
||||
if existing, ok := p.store.Get(state.GetPayoutId()); ok && existing != nil {
|
||||
if existing.GetCreatedAt() != nil {
|
||||
state.CreatedAt = existing.GetCreatedAt()
|
||||
}
|
||||
}
|
||||
|
||||
client := monetix.NewClient(p.config, p.httpClient, p.logger)
|
||||
apiReq := buildCardPayoutRequest(projectID, req)
|
||||
result, err := client.CreateCardPayout(ctx, apiReq)
|
||||
if err != nil {
|
||||
state.Status = mntxv1.PayoutStatus_PAYOUT_STATUS_FAILED
|
||||
state.ProviderMessage = err.Error()
|
||||
state.UpdatedAt = timestamppb.New(p.clock.Now())
|
||||
p.store.Save(state)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
state.ProviderPaymentId = result.ProviderRequestID
|
||||
if result.Accepted {
|
||||
state.Status = mntxv1.PayoutStatus_PAYOUT_STATUS_PENDING
|
||||
} else {
|
||||
state.Status = mntxv1.PayoutStatus_PAYOUT_STATUS_FAILED
|
||||
state.ProviderCode = result.ErrorCode
|
||||
state.ProviderMessage = result.ErrorMessage
|
||||
}
|
||||
state.UpdatedAt = timestamppb.New(p.clock.Now())
|
||||
p.store.Save(state)
|
||||
|
||||
resp := &mntxv1.CardPayoutResponse{
|
||||
Payout: state,
|
||||
Accepted: result.Accepted,
|
||||
ProviderRequestId: result.ProviderRequestID,
|
||||
ErrorCode: result.ErrorCode,
|
||||
ErrorMessage: result.ErrorMessage,
|
||||
}
|
||||
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func (p *cardPayoutProcessor) SubmitToken(ctx context.Context, req *mntxv1.CardTokenPayoutRequest) (*mntxv1.CardTokenPayoutResponse, error) {
|
||||
if p == nil {
|
||||
return nil, merrors.Internal("card payout processor not initialised")
|
||||
}
|
||||
if strings.TrimSpace(p.config.BaseURL) == "" || strings.TrimSpace(p.config.SecretKey) == "" {
|
||||
return nil, merrors.Internal("monetix configuration is incomplete")
|
||||
}
|
||||
|
||||
req = sanitizeCardTokenPayoutRequest(req)
|
||||
if err := validateCardTokenPayoutRequest(req, p.config); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
projectID := req.GetProjectId()
|
||||
if projectID == 0 {
|
||||
projectID = p.config.ProjectID
|
||||
}
|
||||
if projectID == 0 {
|
||||
return nil, merrors.Internal("monetix project_id is not configured")
|
||||
}
|
||||
|
||||
now := timestamppb.New(p.clock.Now())
|
||||
state := &mntxv1.CardPayoutState{
|
||||
PayoutId: req.GetPayoutId(),
|
||||
ProjectId: projectID,
|
||||
CustomerId: req.GetCustomerId(),
|
||||
AmountMinor: req.GetAmountMinor(),
|
||||
Currency: strings.ToUpper(strings.TrimSpace(req.GetCurrency())),
|
||||
Status: mntxv1.PayoutStatus_PAYOUT_STATUS_PENDING,
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
}
|
||||
|
||||
if existing, ok := p.store.Get(state.GetPayoutId()); ok && existing != nil {
|
||||
if existing.GetCreatedAt() != nil {
|
||||
state.CreatedAt = existing.GetCreatedAt()
|
||||
}
|
||||
}
|
||||
|
||||
client := monetix.NewClient(p.config, p.httpClient, p.logger)
|
||||
apiReq := buildCardTokenPayoutRequest(projectID, req)
|
||||
result, err := client.CreateCardTokenPayout(ctx, apiReq)
|
||||
if err != nil {
|
||||
state.Status = mntxv1.PayoutStatus_PAYOUT_STATUS_FAILED
|
||||
state.ProviderMessage = err.Error()
|
||||
state.UpdatedAt = timestamppb.New(p.clock.Now())
|
||||
p.store.Save(state)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
state.ProviderPaymentId = result.ProviderRequestID
|
||||
if result.Accepted {
|
||||
state.Status = mntxv1.PayoutStatus_PAYOUT_STATUS_PENDING
|
||||
} else {
|
||||
state.Status = mntxv1.PayoutStatus_PAYOUT_STATUS_FAILED
|
||||
state.ProviderCode = result.ErrorCode
|
||||
state.ProviderMessage = result.ErrorMessage
|
||||
}
|
||||
state.UpdatedAt = timestamppb.New(p.clock.Now())
|
||||
p.store.Save(state)
|
||||
|
||||
resp := &mntxv1.CardTokenPayoutResponse{
|
||||
Payout: state,
|
||||
Accepted: result.Accepted,
|
||||
ProviderRequestId: result.ProviderRequestID,
|
||||
ErrorCode: result.ErrorCode,
|
||||
ErrorMessage: result.ErrorMessage,
|
||||
}
|
||||
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func (p *cardPayoutProcessor) Tokenize(ctx context.Context, req *mntxv1.CardTokenizeRequest) (*mntxv1.CardTokenizeResponse, error) {
|
||||
if p == nil {
|
||||
return nil, merrors.Internal("card payout processor not initialised")
|
||||
}
|
||||
cardInput, err := validateCardTokenizeRequest(req, p.config)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
projectID := req.GetProjectId()
|
||||
if projectID == 0 {
|
||||
projectID = p.config.ProjectID
|
||||
}
|
||||
if projectID == 0 {
|
||||
return nil, merrors.Internal("monetix project_id is not configured")
|
||||
}
|
||||
|
||||
req = sanitizeCardTokenizeRequest(req)
|
||||
cardInput = extractTokenizeCard(req)
|
||||
client := monetix.NewClient(p.config, p.httpClient, p.logger)
|
||||
apiReq := buildCardTokenizeRequest(projectID, req, cardInput)
|
||||
result, err := client.CreateCardTokenization(ctx, apiReq)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
resp := &mntxv1.CardTokenizeResponse{
|
||||
RequestId: req.GetRequestId(),
|
||||
Success: result.Accepted,
|
||||
ErrorCode: result.ErrorCode,
|
||||
ErrorMessage: result.ErrorMessage,
|
||||
}
|
||||
resp.Token = result.Token
|
||||
resp.MaskedPan = result.MaskedPAN
|
||||
resp.ExpiryMonth = result.ExpiryMonth
|
||||
resp.ExpiryYear = result.ExpiryYear
|
||||
resp.CardBrand = result.CardBrand
|
||||
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func (p *cardPayoutProcessor) Status(_ context.Context, payoutID string) (*mntxv1.CardPayoutState, error) {
|
||||
if p == nil {
|
||||
return nil, merrors.Internal("card payout processor not initialised")
|
||||
}
|
||||
|
||||
id := strings.TrimSpace(payoutID)
|
||||
if id == "" {
|
||||
return nil, merrors.InvalidArgument("payout_id is required", "payout_id")
|
||||
}
|
||||
|
||||
state, ok := p.store.Get(id)
|
||||
if !ok || state == nil {
|
||||
return nil, merrors.NoData("payout not found")
|
||||
}
|
||||
return state, nil
|
||||
}
|
||||
|
||||
func (p *cardPayoutProcessor) ProcessCallback(ctx context.Context, payload []byte) (int, error) {
|
||||
if p == nil {
|
||||
return http.StatusInternalServerError, merrors.Internal("card payout processor not initialised")
|
||||
}
|
||||
if len(payload) == 0 {
|
||||
return http.StatusBadRequest, merrors.InvalidArgument("callback body is empty")
|
||||
}
|
||||
if strings.TrimSpace(p.config.SecretKey) == "" {
|
||||
return http.StatusInternalServerError, merrors.Internal("monetix secret key is not configured")
|
||||
}
|
||||
|
||||
var cb monetixCallback
|
||||
if err := json.Unmarshal(payload, &cb); err != nil {
|
||||
return http.StatusBadRequest, err
|
||||
}
|
||||
|
||||
if strings.TrimSpace(cb.Signature) == "" {
|
||||
return http.StatusBadRequest, merrors.InvalidArgument("signature is missing")
|
||||
}
|
||||
if err := verifyCallbackSignature(cb, p.config.SecretKey); err != nil {
|
||||
p.logger.Warn("Monetix callback signature check failed", zap.Error(err))
|
||||
return http.StatusForbidden, err
|
||||
}
|
||||
|
||||
state, statusLabel := mapCallbackToState(p.clock, p.config, cb)
|
||||
if existing, ok := p.store.Get(state.GetPayoutId()); ok && existing != nil {
|
||||
if existing.GetCreatedAt() != nil {
|
||||
state.CreatedAt = existing.GetCreatedAt()
|
||||
}
|
||||
}
|
||||
p.store.Save(state)
|
||||
p.emitCardPayoutEvent(state)
|
||||
monetix.ObserveCallback(statusLabel)
|
||||
|
||||
p.logger.Info("Monetix payout callback processed",
|
||||
zap.String("payout_id", state.GetPayoutId()),
|
||||
zap.String("status", statusLabel),
|
||||
zap.String("provider_code", state.GetProviderCode()),
|
||||
zap.String("provider_message", state.GetProviderMessage()),
|
||||
zap.String("masked_account", cb.Account.Number),
|
||||
)
|
||||
|
||||
return http.StatusOK, nil
|
||||
}
|
||||
|
||||
func (p *cardPayoutProcessor) emitCardPayoutEvent(state *mntxv1.CardPayoutState) {
|
||||
if state == nil || p.producer == nil {
|
||||
return
|
||||
}
|
||||
|
||||
event := &mntxv1.CardPayoutStatusChangedEvent{Payout: state}
|
||||
payload, err := protojson.Marshal(event)
|
||||
if err != nil {
|
||||
p.logger.Warn("failed to marshal payout callback event", zap.Error(err))
|
||||
return
|
||||
}
|
||||
|
||||
env := messaging.CreateEnvelope(string(mservice.MntxGateway), model.NewNotification(mservice.MntxGateway, nm.NAUpdated))
|
||||
if _, err := env.Wrap(payload); err != nil {
|
||||
p.logger.Warn("failed to wrap payout callback event payload", zap.Error(err))
|
||||
return
|
||||
}
|
||||
if err := p.producer.SendMessage(env); err != nil {
|
||||
p.logger.Warn("failed to publish payout callback event", zap.Error(err))
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user