package service import ( "context" "net/http" "strings" "time" "cmr-backend/internal/apperr" "cmr-backend/internal/platform/security" "cmr-backend/internal/store/postgres" ) type EventService struct { store *postgres.Store } type EventDetailResult struct { Event struct { ID string `json:"id"` Slug string `json:"slug"` DisplayName string `json:"displayName"` Summary *string `json:"summary,omitempty"` Status string `json:"status"` } `json:"event"` Release *struct { ID string `json:"id"` ConfigLabel string `json:"configLabel"` ManifestURL string `json:"manifestUrl"` ManifestChecksumSha256 *string `json:"manifestChecksumSha256,omitempty"` RouteCode *string `json:"routeCode,omitempty"` } `json:"release,omitempty"` ResolvedRelease *ResolvedReleaseView `json:"resolvedRelease,omitempty"` } type LaunchEventInput struct { EventPublicID string UserID string ReleaseID string `json:"releaseId,omitempty"` ClientType string `json:"clientType"` DeviceKey string `json:"deviceKey"` } type LaunchEventResult struct { Event struct { ID string `json:"id"` DisplayName string `json:"displayName"` } `json:"event"` Launch struct { Source string `json:"source"` ResolvedRelease *ResolvedReleaseView `json:"resolvedRelease,omitempty"` Config struct { ConfigURL string `json:"configUrl"` ConfigLabel string `json:"configLabel"` ConfigChecksumSha256 *string `json:"configChecksumSha256,omitempty"` ReleaseID string `json:"releaseId"` RouteCode *string `json:"routeCode,omitempty"` } `json:"config"` Business struct { Source string `json:"source"` EventID string `json:"eventId"` SessionID string `json:"sessionId"` SessionToken string `json:"sessionToken"` SessionTokenExpiresAt string `json:"sessionTokenExpiresAt"` RouteCode *string `json:"routeCode,omitempty"` } `json:"business"` } `json:"launch"` } func NewEventService(store *postgres.Store) *EventService { return &EventService{store: store} } func (s *EventService) GetEventDetail(ctx context.Context, eventPublicID string) (*EventDetailResult, error) { eventPublicID = strings.TrimSpace(eventPublicID) if eventPublicID == "" { return nil, apperr.New(http.StatusBadRequest, "invalid_params", "event id is required") } event, err := s.store.GetEventByPublicID(ctx, eventPublicID) if err != nil { return nil, err } if event == nil { return nil, apperr.New(http.StatusNotFound, "event_not_found", "event not found") } result := &EventDetailResult{} result.Event.ID = event.PublicID result.Event.Slug = event.Slug result.Event.DisplayName = event.DisplayName result.Event.Summary = event.Summary result.Event.Status = event.Status if event.CurrentReleasePubID != nil && event.ConfigLabel != nil && event.ManifestURL != nil { result.Release = &struct { ID string `json:"id"` ConfigLabel string `json:"configLabel"` ManifestURL string `json:"manifestUrl"` ManifestChecksumSha256 *string `json:"manifestChecksumSha256,omitempty"` RouteCode *string `json:"routeCode,omitempty"` }{ ID: *event.CurrentReleasePubID, ConfigLabel: *event.ConfigLabel, ManifestURL: *event.ManifestURL, ManifestChecksumSha256: event.ManifestChecksum, RouteCode: event.RouteCode, } } result.ResolvedRelease = buildResolvedReleaseFromEvent(event, LaunchSourceEventCurrentRelease) return result, nil } func (s *EventService) LaunchEvent(ctx context.Context, input LaunchEventInput) (*LaunchEventResult, error) { input.EventPublicID = strings.TrimSpace(input.EventPublicID) input.ReleaseID = strings.TrimSpace(input.ReleaseID) input.DeviceKey = strings.TrimSpace(input.DeviceKey) if err := validateClientType(input.ClientType); err != nil { return nil, err } if input.EventPublicID == "" || input.UserID == "" || input.DeviceKey == "" { return nil, apperr.New(http.StatusBadRequest, "invalid_params", "event id, user and deviceKey are required") } event, err := s.store.GetEventByPublicID(ctx, input.EventPublicID) if err != nil { return nil, err } if event == nil { return nil, apperr.New(http.StatusNotFound, "event_not_found", "event not found") } if event.Status != "active" { return nil, apperr.New(http.StatusConflict, "event_not_launchable", "event is not active") } if event.CurrentReleaseID == nil || event.CurrentReleasePubID == nil || event.ConfigLabel == nil || event.ManifestURL == nil { return nil, apperr.New(http.StatusConflict, "event_release_missing", "event does not have a published release") } if input.ReleaseID != "" && input.ReleaseID != *event.CurrentReleasePubID { return nil, apperr.New(http.StatusConflict, "release_not_launchable", "requested release is not the current published release") } tx, err := s.store.Begin(ctx) if err != nil { return nil, err } defer tx.Rollback(ctx) sessionPublicID, err := security.GeneratePublicID("sess") if err != nil { return nil, err } sessionToken, err := security.GenerateToken(32) if err != nil { return nil, err } sessionTokenExpiresAt := time.Now().UTC().Add(2 * time.Hour) session, err := s.store.CreateGameSession(ctx, tx, postgres.CreateGameSessionParams{ SessionPublicID: sessionPublicID, UserID: input.UserID, EventID: event.ID, EventReleaseID: *event.CurrentReleaseID, DeviceKey: input.DeviceKey, ClientType: input.ClientType, RouteCode: event.RouteCode, SessionTokenHash: security.HashText(sessionToken), SessionTokenExpiresAt: sessionTokenExpiresAt, }) if err != nil { return nil, err } if err := tx.Commit(ctx); err != nil { return nil, err } result := &LaunchEventResult{} result.Event.ID = event.PublicID result.Event.DisplayName = event.DisplayName result.Launch.Source = LaunchSourceEventCurrentRelease result.Launch.ResolvedRelease = buildResolvedReleaseFromEvent(event, LaunchSourceEventCurrentRelease) result.Launch.Config.ConfigURL = *event.ManifestURL result.Launch.Config.ConfigLabel = *event.ConfigLabel result.Launch.Config.ConfigChecksumSha256 = event.ManifestChecksum result.Launch.Config.ReleaseID = *event.CurrentReleasePubID result.Launch.Config.RouteCode = event.RouteCode result.Launch.Business.Source = "direct-event" result.Launch.Business.EventID = event.PublicID result.Launch.Business.SessionID = session.SessionPublicID result.Launch.Business.SessionToken = sessionToken result.Launch.Business.SessionTokenExpiresAt = session.SessionTokenExpiresAt.Format(time.RFC3339) result.Launch.Business.RouteCode = event.RouteCode return result, nil }