Files
sendico/api/gateway/mntx/internal/service/gateway/callback.go
2025-12-04 21:16:15 +01:00

135 lines
4.1 KiB
Go

package gateway
import (
"context"
"crypto/hmac"
"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"
"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"`
Date string `json:"date"`
AuthCode string `json:"auth_code"`
} `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"`
} `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) {
if s.card == nil {
return http.StatusInternalServerError, merrors.Internal("card payout processor not initialised")
}
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(cb monetixCallback, secret string) error {
expected := cb.Signature
cb.Signature = ""
calculated, err := monetix.SignPayload(cb, secret)
if err != nil {
return err
}
if subtleConstantTimeCompare(expected, calculated) {
return nil
}
return merrors.DataConflict("signature mismatch")
}
func subtleConstantTimeCompare(a, b string) bool {
return hmac.Equal([]byte(strings.TrimSpace(a)), []byte(strings.TrimSpace(b)))
}