package gateway import ( "bytes" "context" "crypto/hmac" "encoding/json" "net/http" "strings" "github.com/tech/sendico/gateway/aurora/internal/service/provider" 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 providerCallback 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"` } // ProcessProviderCallback ingests provider callbacks and updates payout state. func (s *Service) ProcessProviderCallback(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 provider.Config, cb providerCallback) (*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 := provider.OutcomeDecline internalStatus := mntxv1.PayoutStatus_PAYOUT_STATUS_FAILED if status == cfg.SuccessStatus() && opStatus == cfg.SuccessStatus() && (code == "" || code == "0") { internalStatus = mntxv1.PayoutStatus_PAYOUT_STATUS_SUCCESS outcome = provider.OutcomeSuccess } else if status == cfg.ProcessingStatus() || opStatus == cfg.ProcessingStatus() { internalStatus = mntxv1.PayoutStatus_PAYOUT_STATUS_WAITING outcome = provider.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), OperationRef: strings.TrimSpace(cb.Payment.ID), UpdatedAt: now, CreatedAt: now, } return state, outcome } func fallbackProviderPaymentID(cb providerCallback) 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 := provider.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))) }