event_service.go 9.4 KB

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