event_service.go 9.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252
  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 event.Status != "active" {
  142. return nil, apperr.New(http.StatusConflict, "event_not_launchable", "event is not active")
  143. }
  144. if event.CurrentReleaseID == nil || event.CurrentReleasePubID == nil || event.ConfigLabel == nil || event.ManifestURL == nil {
  145. return nil, apperr.New(http.StatusConflict, "event_release_missing", "event does not have a published release")
  146. }
  147. if input.ReleaseID != "" && input.ReleaseID != *event.CurrentReleasePubID {
  148. return nil, apperr.New(http.StatusConflict, "release_not_launchable", "requested release is not the current published release")
  149. }
  150. variantPlan := resolveVariantPlan(event.ReleasePayloadJSON)
  151. variant, err := resolveLaunchVariant(variantPlan, input.VariantID)
  152. if err != nil {
  153. return nil, err
  154. }
  155. routeCode := event.RouteCode
  156. var assignmentMode *string
  157. var variantID *string
  158. var variantName *string
  159. if variant != nil {
  160. resultMode := variant.AssignmentMode
  161. assignmentMode = &resultMode
  162. variantID = &variant.ID
  163. variantName = &variant.Name
  164. if variant.RouteCode != nil {
  165. routeCode = variant.RouteCode
  166. }
  167. }
  168. tx, err := s.store.Begin(ctx)
  169. if err != nil {
  170. return nil, err
  171. }
  172. defer tx.Rollback(ctx)
  173. sessionPublicID, err := security.GeneratePublicID("sess")
  174. if err != nil {
  175. return nil, err
  176. }
  177. sessionToken, err := security.GenerateToken(32)
  178. if err != nil {
  179. return nil, err
  180. }
  181. sessionTokenExpiresAt := time.Now().UTC().Add(2 * time.Hour)
  182. session, err := s.store.CreateGameSession(ctx, tx, postgres.CreateGameSessionParams{
  183. SessionPublicID: sessionPublicID,
  184. UserID: input.UserID,
  185. EventID: event.ID,
  186. EventReleaseID: *event.CurrentReleaseID,
  187. DeviceKey: input.DeviceKey,
  188. ClientType: input.ClientType,
  189. AssignmentMode: assignmentMode,
  190. VariantID: variantID,
  191. VariantName: variantName,
  192. RouteCode: routeCode,
  193. SessionTokenHash: security.HashText(sessionToken),
  194. SessionTokenExpiresAt: sessionTokenExpiresAt,
  195. })
  196. if err != nil {
  197. return nil, err
  198. }
  199. if err := tx.Commit(ctx); err != nil {
  200. return nil, err
  201. }
  202. result := &LaunchEventResult{}
  203. result.Event.ID = event.PublicID
  204. result.Event.DisplayName = event.DisplayName
  205. result.Launch.Source = LaunchSourceEventCurrentRelease
  206. result.Launch.ResolvedRelease = buildResolvedReleaseFromEvent(event, LaunchSourceEventCurrentRelease)
  207. result.Launch.Variant = variant
  208. result.Launch.Runtime = buildRuntimeSummaryFromEvent(event)
  209. result.Launch.Presentation = buildPresentationSummaryFromEvent(event)
  210. if enrichedPresentation, err := loadPresentationSummaryByPublicID(ctx, s.store, event.PresentationID); err != nil {
  211. return nil, err
  212. } else if enrichedPresentation != nil {
  213. result.Launch.Presentation = enrichedPresentation
  214. }
  215. result.Launch.ContentBundle = buildContentBundleSummaryFromEvent(event)
  216. if enrichedBundle, err := loadContentBundleSummaryByPublicID(ctx, s.store, event.ContentBundleID); err != nil {
  217. return nil, err
  218. } else if enrichedBundle != nil {
  219. result.Launch.ContentBundle = enrichedBundle
  220. }
  221. result.Launch.Config.ConfigURL = *event.ManifestURL
  222. result.Launch.Config.ConfigLabel = *event.ConfigLabel
  223. result.Launch.Config.ConfigChecksumSha256 = event.ManifestChecksum
  224. result.Launch.Config.ReleaseID = *event.CurrentReleasePubID
  225. result.Launch.Config.RouteCode = routeCode
  226. result.Launch.Business.Source = "direct-event"
  227. result.Launch.Business.EventID = event.PublicID
  228. result.Launch.Business.SessionID = session.SessionPublicID
  229. result.Launch.Business.SessionToken = sessionToken
  230. result.Launch.Business.SessionTokenExpiresAt = session.SessionTokenExpiresAt.Format(time.RFC3339)
  231. result.Launch.Business.RouteCode = routeCode
  232. return result, nil
  233. }