package gateway import ( "context" "net/http" "strings" "github.com/google/uuid" "github.com/tech/sendico/gateway/mntx/internal/service/monetix" "github.com/tech/sendico/pkg/api/routers" "github.com/tech/sendico/pkg/api/routers/gsresponse" clockpkg "github.com/tech/sendico/pkg/clock" msg "github.com/tech/sendico/pkg/messaging" "github.com/tech/sendico/pkg/mlogger" "github.com/tech/sendico/pkg/mservice" mntxv1 "github.com/tech/sendico/pkg/proto/gateway/mntx/v1" "go.uber.org/zap" "google.golang.org/grpc" ) type Service struct { logger mlogger.Logger clock clockpkg.Clock producer msg.Producer store *payoutStore cardStore *cardPayoutStore config monetix.Config httpClient *http.Client card *cardPayoutProcessor mntxv1.UnimplementedMntxGatewayServiceServer } type payoutFailure interface { error Reason() string } type reasonedError struct { reason string err error } func (r reasonedError) Error() string { return r.err.Error() } func (r reasonedError) Unwrap() error { return r.err } func (r reasonedError) Reason() string { return r.reason } // NewService constructs the Monetix gateway service skeleton. func NewService(logger mlogger.Logger, opts ...Option) *Service { svc := &Service{ logger: logger.Named("service"), clock: clockpkg.NewSystem(), store: newPayoutStore(), cardStore: newCardPayoutStore(), config: monetix.DefaultConfig(), } initMetrics() for _, opt := range opts { if opt != nil { opt(svc) } } if svc.clock == nil { svc.clock = clockpkg.NewSystem() } if svc.httpClient == nil { svc.httpClient = &http.Client{Timeout: svc.config.Timeout()} } else if svc.httpClient.Timeout <= 0 { svc.httpClient.Timeout = svc.config.Timeout() } if svc.cardStore == nil { svc.cardStore = newCardPayoutStore() } svc.card = newCardPayoutProcessor(svc.logger, svc.config, svc.clock, svc.cardStore, svc.httpClient, svc.producer) return svc } // Register wires the service onto the provided gRPC router. func (s *Service) Register(router routers.GRPC) error { return router.Register(func(reg grpc.ServiceRegistrar) { mntxv1.RegisterMntxGatewayServiceServer(reg, s) }) } func executeUnary[TReq any, TResp any](ctx context.Context, svc *Service, method string, handler func(context.Context, *TReq) gsresponse.Responder[TResp], req *TReq) (*TResp, error) { log := svc.logger.Named("rpc") log.Info("RPC request started", zap.String("method", method)) start := svc.clock.Now() resp, err := gsresponse.Unary(svc.logger, mservice.MntxGateway, handler)(ctx, req) duration := svc.clock.Now().Sub(start) observeRPC(method, err, duration) if err != nil { log.Warn("RPC request failed", zap.String("method", method), zap.Duration("duration", duration), zap.Error(err)) } else { log.Info("RPC request completed", zap.String("method", method), zap.Duration("duration", duration)) } return resp, err } func newPayoutRef() string { return "pyt_" + strings.ReplaceAll(uuid.New().String(), "-", "") } func normalizeReason(reason string) string { return strings.ToLower(strings.TrimSpace(reason)) } func newPayoutError(reason string, err error) error { return reasonedError{ reason: normalizeReason(reason), err: err, } }