package ports import ( "context" "fmt" "strings" "time" "galaxy/user/internal/domain/common" ) // UserLifecycleEventType identifies one user-lifecycle event kind propagated // to `Game Lobby` through the dedicated Redis Stream. type UserLifecycleEventType string const ( // UserLifecyclePermanentBlockedEventType identifies the post-commit event // emitted when `SanctionCodePermanentBlock` becomes active on an account. UserLifecyclePermanentBlockedEventType UserLifecycleEventType = "user.lifecycle.permanent_blocked" // UserLifecycleDeletedEventType identifies the post-commit event emitted // when a trusted `DeleteUser` command soft-deletes an account. UserLifecycleDeletedEventType UserLifecycleEventType = "user.lifecycle.deleted" ) // IsKnown reports whether the event type belongs to the frozen vocabulary. func (eventType UserLifecycleEventType) IsKnown() bool { switch eventType { case UserLifecyclePermanentBlockedEventType, UserLifecycleDeletedEventType: return true default: return false } } // UserLifecycleEvent stores one post-commit user-lifecycle event envelope // published to the `user:lifecycle_events` Redis Stream and consumed by // `Game Lobby` for Race Name Directory cascade release. type UserLifecycleEvent struct { // EventType stores the frozen lifecycle event discriminator. EventType UserLifecycleEventType // UserID identifies the regular user whose lifecycle state changed. UserID common.UserID // OccurredAt stores the committed mutation timestamp. OccurredAt time.Time // Source stores the machine-readable mutation source. For Stage 22 this is // always `admin_internal_api`. Source common.Source // Actor stores the audit actor metadata attached to the committed // mutation. Actor common.ActorRef // ReasonCode stores the committed reason_code for the mutation. ReasonCode common.ReasonCode // TraceID stores the optional OpenTelemetry trace identifier propagated // from the current request context. TraceID string } // Validate reports whether event is structurally complete. func (event UserLifecycleEvent) Validate() error { if !event.EventType.IsKnown() { return fmt.Errorf("user lifecycle event type %q is unsupported", event.EventType) } if err := event.UserID.Validate(); err != nil { return fmt.Errorf("user lifecycle event user id: %w", err) } if err := common.ValidateTimestamp("user lifecycle event occurred at", event.OccurredAt); err != nil { return err } if err := event.Source.Validate(); err != nil { return fmt.Errorf("user lifecycle event source: %w", err) } if err := event.Actor.Validate(); err != nil { return fmt.Errorf("user lifecycle event actor: %w", err) } if err := event.ReasonCode.Validate(); err != nil { return fmt.Errorf("user lifecycle event reason code: %w", err) } if event.TraceID != "" && strings.TrimSpace(event.TraceID) != event.TraceID { return fmt.Errorf("user lifecycle event trace id must not contain surrounding whitespace") } return nil } // UserLifecyclePublisher publishes one committed user-lifecycle event to the // dedicated `user:lifecycle_events` Redis Stream. type UserLifecyclePublisher interface { // PublishUserLifecycleEvent propagates one committed lifecycle event. The // implementation must validate the event envelope and perform exactly one // idempotent append per call. PublishUserLifecycleEvent(ctx context.Context, event UserLifecycleEvent) error }