event_service.go 9.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249
  1. package service
  2. import (
  3. "context"
  4. "net/http"
  5. "strings"
  6. "time"
  7. "cmr-backend/internal/apperr"
  8. "cmr-backend/internal/platform/security"
  9. "cmr-backend/internal/store/postgres"
  10. )
  11. type EventService struct {
  12. store *postgres.Store
  13. }
  14. type EventDetailResult struct {
  15. Event struct {
  16. ID string `json:"id"`
  17. Slug string `json:"slug"`
  18. DisplayName string `json:"displayName"`
  19. Summary *string `json:"summary,omitempty"`
  20. Status string `json:"status"`
  21. } `json:"event"`
  22. Release *struct {
  23. ID string `json:"id"`
  24. ConfigLabel string `json:"configLabel"`
  25. ManifestURL string `json:"manifestUrl"`
  26. ManifestChecksumSha256 *string `json:"manifestChecksumSha256,omitempty"`
  27. RouteCode *string `json:"routeCode,omitempty"`
  28. } `json:"release,omitempty"`
  29. ResolvedRelease *ResolvedReleaseView `json:"resolvedRelease,omitempty"`
  30. Runtime *RuntimeSummaryView `json:"runtime,omitempty"`
  31. CurrentPresentation *PresentationSummaryView `json:"currentPresentation,omitempty"`
  32. CurrentContentBundle *ContentBundleSummaryView `json:"currentContentBundle,omitempty"`
  33. }
  34. type LaunchEventInput struct {
  35. EventPublicID string
  36. UserID string
  37. ReleaseID string `json:"releaseId,omitempty"`
  38. VariantID string `json:"variantId,omitempty"`
  39. ClientType string `json:"clientType"`
  40. DeviceKey string `json:"deviceKey"`
  41. }
  42. type LaunchEventResult struct {
  43. Event struct {
  44. ID string `json:"id"`
  45. DisplayName string `json:"displayName"`
  46. } `json:"event"`
  47. Launch struct {
  48. Source string `json:"source"`
  49. ResolvedRelease *ResolvedReleaseView `json:"resolvedRelease,omitempty"`
  50. Variant *VariantBindingView `json:"variant,omitempty"`
  51. Runtime *RuntimeSummaryView `json:"runtime,omitempty"`
  52. Presentation *PresentationSummaryView `json:"presentation,omitempty"`
  53. ContentBundle *ContentBundleSummaryView `json:"contentBundle,omitempty"`
  54. Config struct {
  55. ConfigURL string `json:"configUrl"`
  56. ConfigLabel string `json:"configLabel"`
  57. ConfigChecksumSha256 *string `json:"configChecksumSha256,omitempty"`
  58. ReleaseID string `json:"releaseId"`
  59. RouteCode *string `json:"routeCode,omitempty"`
  60. } `json:"config"`
  61. Business struct {
  62. Source string `json:"source"`
  63. EventID string `json:"eventId"`
  64. SessionID string `json:"sessionId"`
  65. SessionToken string `json:"sessionToken"`
  66. SessionTokenExpiresAt string `json:"sessionTokenExpiresAt"`
  67. RouteCode *string `json:"routeCode,omitempty"`
  68. } `json:"business"`
  69. } `json:"launch"`
  70. }
  71. func NewEventService(store *postgres.Store) *EventService {
  72. return &EventService{store: store}
  73. }
  74. func (s *EventService) GetEventDetail(ctx context.Context, eventPublicID string) (*EventDetailResult, error) {
  75. eventPublicID = strings.TrimSpace(eventPublicID)
  76. if eventPublicID == "" {
  77. return nil, apperr.New(http.StatusBadRequest, "invalid_params", "event id is required")
  78. }
  79. event, err := s.store.GetEventByPublicID(ctx, eventPublicID)
  80. if err != nil {
  81. return nil, err
  82. }
  83. if event == nil {
  84. return nil, apperr.New(http.StatusNotFound, "event_not_found", "event not found")
  85. }
  86. result := &EventDetailResult{}
  87. result.Event.ID = event.PublicID
  88. result.Event.Slug = event.Slug
  89. result.Event.DisplayName = event.DisplayName
  90. result.Event.Summary = event.Summary
  91. result.Event.Status = event.Status
  92. if event.CurrentReleasePubID != nil && event.ConfigLabel != nil && event.ManifestURL != nil {
  93. result.Release = &struct {
  94. ID string `json:"id"`
  95. ConfigLabel string `json:"configLabel"`
  96. ManifestURL string `json:"manifestUrl"`
  97. ManifestChecksumSha256 *string `json:"manifestChecksumSha256,omitempty"`
  98. RouteCode *string `json:"routeCode,omitempty"`
  99. }{
  100. ID: *event.CurrentReleasePubID,
  101. ConfigLabel: *event.ConfigLabel,
  102. ManifestURL: *event.ManifestURL,
  103. ManifestChecksumSha256: event.ManifestChecksum,
  104. RouteCode: event.RouteCode,
  105. }
  106. }
  107. result.ResolvedRelease = buildResolvedReleaseFromEvent(event, LaunchSourceEventCurrentRelease)
  108. result.Runtime = buildRuntimeSummaryFromEvent(event)
  109. result.CurrentPresentation = buildPresentationSummaryFromEvent(event)
  110. if enrichedPresentation, err := loadPresentationSummaryByPublicID(ctx, s.store, event.PresentationID); err != nil {
  111. return nil, err
  112. } else if enrichedPresentation != nil {
  113. result.CurrentPresentation = enrichedPresentation
  114. }
  115. result.CurrentContentBundle = buildContentBundleSummaryFromEvent(event)
  116. if enrichedBundle, err := loadContentBundleSummaryByPublicID(ctx, s.store, event.ContentBundleID); err != nil {
  117. return nil, err
  118. } else if enrichedBundle != nil {
  119. result.CurrentContentBundle = enrichedBundle
  120. }
  121. return result, nil
  122. }
  123. func (s *EventService) LaunchEvent(ctx context.Context, input LaunchEventInput) (*LaunchEventResult, error) {
  124. input.EventPublicID = strings.TrimSpace(input.EventPublicID)
  125. input.ReleaseID = strings.TrimSpace(input.ReleaseID)
  126. input.VariantID = strings.TrimSpace(input.VariantID)
  127. input.DeviceKey = strings.TrimSpace(input.DeviceKey)
  128. if err := validateClientType(input.ClientType); err != nil {
  129. return nil, err
  130. }
  131. if input.EventPublicID == "" || input.UserID == "" || input.DeviceKey == "" {
  132. return nil, apperr.New(http.StatusBadRequest, "invalid_params", "event id, user and deviceKey are required")
  133. }
  134. event, err := s.store.GetEventByPublicID(ctx, input.EventPublicID)
  135. if err != nil {
  136. return nil, err
  137. }
  138. if event == nil {
  139. return nil, apperr.New(http.StatusNotFound, "event_not_found", "event not found")
  140. }
  141. if canLaunch, reason := evaluateEventLaunchReadiness(event); !canLaunch {
  142. return nil, launchReadinessError(reason)
  143. }
  144. if input.ReleaseID != "" && input.ReleaseID != *event.CurrentReleasePubID {
  145. return nil, apperr.New(http.StatusConflict, "release_not_launchable", "requested release is not the current published release")
  146. }
  147. variantPlan := resolveVariantPlan(event.ReleasePayloadJSON)
  148. variant, err := resolveLaunchVariant(variantPlan, input.VariantID)
  149. if err != nil {
  150. return nil, err
  151. }
  152. routeCode := event.RouteCode
  153. var assignmentMode *string
  154. var variantID *string
  155. var variantName *string
  156. if variant != nil {
  157. resultMode := variant.AssignmentMode
  158. assignmentMode = &resultMode
  159. variantID = &variant.ID
  160. variantName = &variant.Name
  161. if variant.RouteCode != nil {
  162. routeCode = variant.RouteCode
  163. }
  164. }
  165. tx, err := s.store.Begin(ctx)
  166. if err != nil {
  167. return nil, err
  168. }
  169. defer tx.Rollback(ctx)
  170. sessionPublicID, err := security.GeneratePublicID("sess")
  171. if err != nil {
  172. return nil, err
  173. }
  174. sessionToken, err := security.GenerateToken(32)
  175. if err != nil {
  176. return nil, err
  177. }
  178. sessionTokenExpiresAt := time.Now().UTC().Add(2 * time.Hour)
  179. session, err := s.store.CreateGameSession(ctx, tx, postgres.CreateGameSessionParams{
  180. SessionPublicID: sessionPublicID,
  181. UserID: input.UserID,
  182. EventID: event.ID,
  183. EventReleaseID: *event.CurrentReleaseID,
  184. DeviceKey: input.DeviceKey,
  185. ClientType: input.ClientType,
  186. AssignmentMode: assignmentMode,
  187. VariantID: variantID,
  188. VariantName: variantName,
  189. RouteCode: routeCode,
  190. SessionTokenHash: security.HashText(sessionToken),
  191. SessionTokenExpiresAt: sessionTokenExpiresAt,
  192. })
  193. if err != nil {
  194. return nil, err
  195. }
  196. if err := tx.Commit(ctx); err != nil {
  197. return nil, err
  198. }
  199. result := &LaunchEventResult{}
  200. result.Event.ID = event.PublicID
  201. result.Event.DisplayName = event.DisplayName
  202. result.Launch.Source = LaunchSourceEventCurrentRelease
  203. result.Launch.ResolvedRelease = buildResolvedReleaseFromEvent(event, LaunchSourceEventCurrentRelease)
  204. result.Launch.Variant = variant
  205. result.Launch.Runtime = buildRuntimeSummaryFromEvent(event)
  206. result.Launch.Presentation = buildPresentationSummaryFromEvent(event)
  207. if enrichedPresentation, err := loadPresentationSummaryByPublicID(ctx, s.store, event.PresentationID); err != nil {
  208. return nil, err
  209. } else if enrichedPresentation != nil {
  210. result.Launch.Presentation = enrichedPresentation
  211. }
  212. result.Launch.ContentBundle = buildContentBundleSummaryFromEvent(event)
  213. if enrichedBundle, err := loadContentBundleSummaryByPublicID(ctx, s.store, event.ContentBundleID); err != nil {
  214. return nil, err
  215. } else if enrichedBundle != nil {
  216. result.Launch.ContentBundle = enrichedBundle
  217. }
  218. result.Launch.Config.ConfigURL = *event.ManifestURL
  219. result.Launch.Config.ConfigLabel = *event.ConfigLabel
  220. result.Launch.Config.ConfigChecksumSha256 = event.ManifestChecksum
  221. result.Launch.Config.ReleaseID = *event.CurrentReleasePubID
  222. result.Launch.Config.RouteCode = routeCode
  223. result.Launch.Business.Source = "direct-event"
  224. result.Launch.Business.EventID = event.PublicID
  225. result.Launch.Business.SessionID = session.SessionPublicID
  226. result.Launch.Business.SessionToken = sessionToken
  227. result.Launch.Business.SessionTokenExpiresAt = session.SessionTokenExpiresAt.Format(time.RFC3339)
  228. result.Launch.Business.RouteCode = routeCode
  229. return result, nil
  230. }