| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249 |
- 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"`
- Runtime *RuntimeSummaryView `json:"runtime,omitempty"`
- CurrentPresentation *PresentationSummaryView `json:"currentPresentation,omitempty"`
- CurrentContentBundle *ContentBundleSummaryView `json:"currentContentBundle,omitempty"`
- }
- type LaunchEventInput struct {
- EventPublicID string
- UserID string
- ReleaseID string `json:"releaseId,omitempty"`
- VariantID string `json:"variantId,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"`
- Variant *VariantBindingView `json:"variant,omitempty"`
- Runtime *RuntimeSummaryView `json:"runtime,omitempty"`
- Presentation *PresentationSummaryView `json:"presentation,omitempty"`
- ContentBundle *ContentBundleSummaryView `json:"contentBundle,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)
- result.Runtime = buildRuntimeSummaryFromEvent(event)
- result.CurrentPresentation = buildPresentationSummaryFromEvent(event)
- if enrichedPresentation, err := loadPresentationSummaryByPublicID(ctx, s.store, event.PresentationID); err != nil {
- return nil, err
- } else if enrichedPresentation != nil {
- result.CurrentPresentation = enrichedPresentation
- }
- result.CurrentContentBundle = buildContentBundleSummaryFromEvent(event)
- if enrichedBundle, err := loadContentBundleSummaryByPublicID(ctx, s.store, event.ContentBundleID); err != nil {
- return nil, err
- } else if enrichedBundle != nil {
- result.CurrentContentBundle = enrichedBundle
- }
- 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.VariantID = strings.TrimSpace(input.VariantID)
- 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 canLaunch, reason := evaluateEventLaunchReadiness(event); !canLaunch {
- return nil, launchReadinessError(reason)
- }
- if input.ReleaseID != "" && input.ReleaseID != *event.CurrentReleasePubID {
- return nil, apperr.New(http.StatusConflict, "release_not_launchable", "requested release is not the current published release")
- }
- variantPlan := resolveVariantPlan(event.ReleasePayloadJSON)
- variant, err := resolveLaunchVariant(variantPlan, input.VariantID)
- if err != nil {
- return nil, err
- }
- routeCode := event.RouteCode
- var assignmentMode *string
- var variantID *string
- var variantName *string
- if variant != nil {
- resultMode := variant.AssignmentMode
- assignmentMode = &resultMode
- variantID = &variant.ID
- variantName = &variant.Name
- if variant.RouteCode != nil {
- routeCode = variant.RouteCode
- }
- }
- 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,
- AssignmentMode: assignmentMode,
- VariantID: variantID,
- VariantName: variantName,
- RouteCode: 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.Variant = variant
- result.Launch.Runtime = buildRuntimeSummaryFromEvent(event)
- result.Launch.Presentation = buildPresentationSummaryFromEvent(event)
- if enrichedPresentation, err := loadPresentationSummaryByPublicID(ctx, s.store, event.PresentationID); err != nil {
- return nil, err
- } else if enrichedPresentation != nil {
- result.Launch.Presentation = enrichedPresentation
- }
- result.Launch.ContentBundle = buildContentBundleSummaryFromEvent(event)
- if enrichedBundle, err := loadContentBundleSummaryByPublicID(ctx, s.store, event.ContentBundleID); err != nil {
- return nil, err
- } else if enrichedBundle != nil {
- result.Launch.ContentBundle = enrichedBundle
- }
- 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 = 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 = routeCode
- return result, nil
- }
|