177 lines
5.3 KiB
Go
177 lines
5.3 KiB
Go
package gateway
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"crypto/hmac"
|
|
"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"
|
|
mntxv1 "github.com/tech/sendico/pkg/proto/gateway/mntx/v1"
|
|
"go.uber.org/zap"
|
|
"google.golang.org/protobuf/types/known/timestamppb"
|
|
)
|
|
|
|
type callbackPayment struct {
|
|
ID string `json:"id"`
|
|
Type string `json:"type"`
|
|
Status string `json:"status"`
|
|
Date string `json:"date"`
|
|
Method string `json:"method"`
|
|
Description string `json:"description"`
|
|
Sum struct {
|
|
Amount int64 `json:"amount"`
|
|
Currency string `json:"currency"`
|
|
} `json:"sum"`
|
|
}
|
|
|
|
type callbackOperation struct {
|
|
ID int64 `json:"id"`
|
|
Type string `json:"type"`
|
|
Status string `json:"status"`
|
|
Date string `json:"date"`
|
|
CreatedDate string `json:"created_date"`
|
|
RequestID string `json:"request_id"`
|
|
SumInitial struct {
|
|
Amount int64 `json:"amount"`
|
|
Currency string `json:"currency"`
|
|
} `json:"sum_initial"`
|
|
SumConverted struct {
|
|
Amount int64 `json:"amount"`
|
|
Currency string `json:"currency"`
|
|
} `json:"sum_converted"`
|
|
Provider struct {
|
|
ID int64 `json:"id"`
|
|
PaymentID string `json:"payment_id"`
|
|
AuthCode string `json:"auth_code"`
|
|
EndpointID int64 `json:"endpoint_id"`
|
|
Date string `json:"date"`
|
|
} `json:"provider"`
|
|
Code string `json:"code"`
|
|
Message string `json:"message"`
|
|
}
|
|
|
|
type monetixCallback struct {
|
|
ProjectID int64 `json:"project_id"`
|
|
Payment callbackPayment `json:"payment"`
|
|
Account struct {
|
|
Number string `json:"number"`
|
|
Type string `json:"type"`
|
|
CardHolder string `json:"card_holder"`
|
|
ExpiryMonth string `json:"expiry_month"`
|
|
ExpiryYear string `json:"expiry_year"`
|
|
} `json:"account"`
|
|
Customer struct {
|
|
ID string `json:"id"`
|
|
} `json:"customer"`
|
|
Operation callbackOperation `json:"operation"`
|
|
Signature string `json:"signature"`
|
|
}
|
|
|
|
// ProcessMonetixCallback ingests Monetix provider callbacks and updates payout state.
|
|
func (s *Service) ProcessMonetixCallback(ctx context.Context, payload []byte) (int, error) {
|
|
log := s.logger.Named("callback")
|
|
if s.card == nil {
|
|
log.Warn("Card payout processor not initialised")
|
|
return http.StatusInternalServerError, merrors.Internal("card payout processor not initialised")
|
|
}
|
|
log.Debug("Callback processing requested", zap.Int("payload_bytes", len(payload)))
|
|
return s.card.ProcessCallback(ctx, payload)
|
|
}
|
|
|
|
func mapCallbackToState(clock clockpkg.Clock, cfg monetix.Config, cb monetixCallback) (*mntxv1.CardPayoutState, string) {
|
|
status := strings.ToLower(strings.TrimSpace(cb.Payment.Status))
|
|
opStatus := strings.ToLower(strings.TrimSpace(cb.Operation.Status))
|
|
code := strings.TrimSpace(cb.Operation.Code)
|
|
|
|
outcome := monetix.OutcomeDecline
|
|
internalStatus := mntxv1.PayoutStatus_PAYOUT_STATUS_FAILED
|
|
|
|
if status == cfg.SuccessStatus() && opStatus == cfg.SuccessStatus() && (code == "" || code == "0") {
|
|
internalStatus = mntxv1.PayoutStatus_PAYOUT_STATUS_PROCESSED
|
|
outcome = monetix.OutcomeSuccess
|
|
} else if status == cfg.ProcessingStatus() || opStatus == cfg.ProcessingStatus() {
|
|
internalStatus = mntxv1.PayoutStatus_PAYOUT_STATUS_PENDING
|
|
outcome = monetix.OutcomeProcessing
|
|
}
|
|
|
|
now := timestamppb.New(clock.Now())
|
|
state := &mntxv1.CardPayoutState{
|
|
PayoutId: cb.Payment.ID,
|
|
ProjectId: cb.ProjectID,
|
|
CustomerId: cb.Customer.ID,
|
|
AmountMinor: cb.Payment.Sum.Amount,
|
|
Currency: strings.ToUpper(strings.TrimSpace(cb.Payment.Sum.Currency)),
|
|
Status: internalStatus,
|
|
ProviderCode: cb.Operation.Code,
|
|
ProviderMessage: cb.Operation.Message,
|
|
ProviderPaymentId: fallbackProviderPaymentID(cb),
|
|
UpdatedAt: now,
|
|
CreatedAt: now,
|
|
}
|
|
|
|
return state, outcome
|
|
}
|
|
|
|
func fallbackProviderPaymentID(cb monetixCallback) string {
|
|
if cb.Operation.Provider.PaymentID != "" {
|
|
return cb.Operation.Provider.PaymentID
|
|
}
|
|
if cb.Operation.RequestID != "" {
|
|
return cb.Operation.RequestID
|
|
}
|
|
return cb.Payment.ID
|
|
}
|
|
|
|
func verifyCallbackSignature(payload []byte, secret string) (string, error) {
|
|
root, err := decodeCallbackPayload(payload)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
signature, ok := signatureFromPayload(root)
|
|
if !ok || strings.TrimSpace(signature) == "" {
|
|
return "", merrors.InvalidArgument("signature is missing")
|
|
}
|
|
calculated, err := monetix.SignPayload(root, secret)
|
|
if err != nil {
|
|
return signature, err
|
|
}
|
|
if subtleConstantTimeCompare(signature, calculated) {
|
|
return signature, nil
|
|
}
|
|
return signature, merrors.DataConflict("signature mismatch")
|
|
}
|
|
|
|
func decodeCallbackPayload(payload []byte) (any, error) {
|
|
var root any
|
|
decoder := json.NewDecoder(bytes.NewReader(payload))
|
|
decoder.UseNumber()
|
|
if err := decoder.Decode(&root); err != nil {
|
|
return nil, err
|
|
}
|
|
return root, nil
|
|
}
|
|
|
|
func signatureFromPayload(root any) (string, bool) {
|
|
payload, ok := root.(map[string]any)
|
|
if !ok {
|
|
return "", false
|
|
}
|
|
for key, value := range payload {
|
|
if !strings.EqualFold(key, "signature") {
|
|
continue
|
|
}
|
|
signature, ok := value.(string)
|
|
return signature, ok
|
|
}
|
|
return "", false
|
|
}
|
|
|
|
func subtleConstantTimeCompare(a, b string) bool {
|
|
return hmac.Equal([]byte(strings.TrimSpace(a)), []byte(strings.TrimSpace(b)))
|
|
}
|