@@ -24,6 +24,7 @@ import (
2424 "github.com/meridianhub/meridian/services/payment-order/adapters/persistence"
2525 "github.com/meridianhub/meridian/services/payment-order/config"
2626 "github.com/meridianhub/meridian/services/payment-order/domain"
27+ poobservability "github.com/meridianhub/meridian/services/payment-order/observability"
2728 "github.com/meridianhub/meridian/services/payment-order/service"
2829 "github.com/meridianhub/meridian/services/payment-order/worker"
2930 sharedclients "github.com/meridianhub/meridian/shared/pkg/clients"
6667// ErrMissingHMACSecret is returned when the WEBHOOK_HMAC_SECRET environment variable is not set.
6768var ErrMissingHMACSecret = errors .New ("WEBHOOK_HMAC_SECRET environment variable is required" )
6869
70+ // ErrRedisRequiredInProduction is returned when Redis is unavailable in production environments.
71+ var ErrRedisRequiredInProduction = errors .New ("redis required for idempotency in production environment" )
72+
6973func main () {
7074 // Initialize structured logging with configurable log level
7175 // Note: bootstrap.NewLogger hardcodes INFO level, so we create logger manually for LOG_LEVEL support
@@ -178,17 +182,32 @@ func run(logger *slog.Logger) error {
178182 }
179183 defer kafkaProducer .Close ()
180184
181- // Create Redis client and idempotency service
182- redisClient , err := createRedisClient (logger )
183- if err != nil {
184- return fmt .Errorf ("failed to create Redis client: %w" , err )
185- }
186- defer func () {
187- if err := redisClient .Close (); err != nil {
188- logger .Error ("failed to close Redis client" , "error" , err )
185+ // Create Redis client and idempotency service.
186+ // In production: fail fast if Redis is unavailable (idempotency is critical).
187+ // In non-production: use NoopService for graceful degradation with metrics.
188+ var idempotencyService idempotency.Service
189+ redisClient , redisErr := createRedisClient (logger )
190+ if redisErr != nil {
191+ if env .IsProduction () {
192+ logger .Error ("CRITICAL: Redis unavailable in production - failing fast" , "error" , redisErr )
193+ return bootstrap .Permanent (fmt .Errorf ("%w: %w" , ErrRedisRequiredInProduction , redisErr ))
189194 }
190- }()
191- idempotencyService := idempotency .NewRedisService (redisClient )
195+ logger .Warn ("Redis not available at startup, using noop idempotency service - DEVELOPMENT ONLY" ,
196+ "error" , redisErr ,
197+ "environment" , os .Getenv ("ENVIRONMENT" ))
198+ idempotencyService = idempotency .NewNoopService (logger )
199+ poobservability .SetNoopIdempotencyActive (true )
200+ poobservability .RecordServiceDegradation (poobservability .ComponentIdempotency , poobservability .DegradationReasonStartupFallback )
201+ } else {
202+ idempotencyService = idempotency .NewRedisService (redisClient )
203+ poobservability .SetNoopIdempotencyActive (false )
204+ logger .Info ("idempotency service initialized with Redis" )
205+ defer func () {
206+ if err := redisClient .Close (); err != nil {
207+ logger .Error ("failed to close Redis client" , "error" , err )
208+ }
209+ }()
210+ }
192211
193212 // Create Starlark handler registry with service client handlers.
194213 // This enables saga scripts to call real services (not mocks).
@@ -297,7 +316,7 @@ func run(logger *slog.Logger) error {
297316 var billingCronScheduler * scheduler.CronScheduler
298317 var dunningWorker * worker.DunningWorker
299318
300- if svcConfig .BillingEnabled {
319+ if svcConfig .BillingEnabled && redisClient != nil {
301320 billingRepo := persistence .NewBillingRepository (db )
302321 billingMetrics := worker .NewBillingMetrics ()
303322
@@ -387,6 +406,8 @@ func run(logger *slog.Logger) error {
387406 "shadow_mode" , svcConfig .BillingShadowMode ,
388407 "dunning_poll_interval" , svcConfig .DunningPollInterval ,
389408 "tenant_id" , tenantID )
409+ } else if svcConfig .BillingEnabled && redisClient == nil {
410+ logger .Warn ("billing workers disabled — Redis unavailable (DEVELOPMENT ONLY)" )
390411 } else {
391412 logger .Info ("billing workers disabled (BILLING_ENABLED=false)" )
392413 }
@@ -456,13 +477,19 @@ func run(logger *slog.Logger) error {
456477 return fmt .Errorf ("failed to create webhook handler: %w" , err )
457478 }
458479
459- // Create Stripe event processor for webhook idempotency and dunning
460- eventProcessor , err := webhookhttp .NewStripeEventProcessor (webhookhttp.StripeEventProcessorConfig {
461- RedisClient : redisClient ,
462- Logger : logger ,
463- })
464- if err != nil {
465- return fmt .Errorf ("failed to create stripe event processor: %w" , err )
480+ // Create Stripe event processor for webhook idempotency and dunning.
481+ // Requires Redis; if Redis is unavailable in non-production, skip Stripe webhook processing.
482+ var eventProcessor * webhookhttp.StripeEventProcessor
483+ if redisClient != nil {
484+ eventProcessor , err = webhookhttp .NewStripeEventProcessor (webhookhttp.StripeEventProcessorConfig {
485+ RedisClient : redisClient ,
486+ Logger : logger ,
487+ })
488+ if err != nil {
489+ return fmt .Errorf ("failed to create stripe event processor: %w" , err )
490+ }
491+ } else {
492+ logger .Warn ("Stripe event processor disabled — Redis unavailable (DEVELOPMENT ONLY)" )
466493 }
467494
468495 // Create Stripe webhook handler (optional - only if STRIPE_API_KEY is configured)
0 commit comments