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))) }