package service import ( "context" "net/http" "strings" "time" "cmr-backend/internal/apperr" "cmr-backend/internal/store/postgres" ) type HomeService struct { store *postgres.Store } type ListCardsInput struct { ChannelCode string ChannelType string PlatformAppID string TenantCode string Slot string Limit int } type CardResult struct { ID string `json:"id"` Type string `json:"type"` Title string `json:"title"` Subtitle *string `json:"subtitle,omitempty"` CoverURL *string `json:"coverUrl,omitempty"` DisplaySlot string `json:"displaySlot"` DisplayPriority int `json:"displayPriority"` Event *struct { ID string `json:"id"` DisplayName string `json:"displayName"` Summary *string `json:"summary,omitempty"` } `json:"event,omitempty"` HTMLURL *string `json:"htmlUrl,omitempty"` } type HomeResult struct { Tenant struct { ID string `json:"id"` Code string `json:"code"` Name string `json:"name"` } `json:"tenant"` Channel struct { ID string `json:"id"` Code string `json:"code"` Type string `json:"type"` PlatformAppID *string `json:"platformAppId,omitempty"` DisplayName string `json:"displayName"` Status string `json:"status"` IsDefault bool `json:"isDefault"` } `json:"channel"` Cards []CardResult `json:"cards"` } func NewHomeService(store *postgres.Store) *HomeService { return &HomeService{store: store} } func (s *HomeService) ListCards(ctx context.Context, input ListCardsInput) ([]CardResult, error) { entry, err := s.resolveEntry(ctx, input) if err != nil { return nil, err } cards, err := s.store.ListCardsForEntry(ctx, entry.TenantID, &entry.ID, normalizeSlot(input.Slot), time.Now().UTC(), input.Limit) if err != nil { return nil, err } return mapCards(cards), nil } func (s *HomeService) GetHome(ctx context.Context, input ListCardsInput) (*HomeResult, error) { entry, err := s.resolveEntry(ctx, input) if err != nil { return nil, err } cards, err := s.store.ListCardsForEntry(ctx, entry.TenantID, &entry.ID, normalizeSlot(input.Slot), time.Now().UTC(), input.Limit) if err != nil { return nil, err } result := &HomeResult{ Cards: mapCards(cards), } result.Tenant.ID = entry.TenantID result.Tenant.Code = entry.TenantCode result.Tenant.Name = entry.TenantName result.Channel.ID = entry.ID result.Channel.Code = entry.ChannelCode result.Channel.Type = entry.ChannelType result.Channel.PlatformAppID = entry.PlatformAppID result.Channel.DisplayName = entry.DisplayName result.Channel.Status = entry.Status result.Channel.IsDefault = entry.IsDefault return result, nil } func (s *HomeService) resolveEntry(ctx context.Context, input ListCardsInput) (*postgres.EntryChannel, error) { entry, err := s.store.FindEntryChannel(ctx, postgres.FindEntryChannelParams{ ChannelCode: strings.TrimSpace(input.ChannelCode), ChannelType: strings.TrimSpace(input.ChannelType), PlatformAppID: strings.TrimSpace(input.PlatformAppID), TenantCode: strings.TrimSpace(input.TenantCode), }) if err != nil { return nil, err } if entry == nil { return nil, apperr.New(http.StatusNotFound, "entry_channel_not_found", "entry channel not found") } return entry, nil } func normalizeSlot(slot string) string { slot = strings.TrimSpace(slot) if slot == "" { return "home_primary" } return slot } func mapCards(cards []postgres.Card) []CardResult { results := make([]CardResult, 0, len(cards)) for _, card := range cards { item := CardResult{ ID: card.PublicID, Type: card.CardType, Title: card.Title, Subtitle: card.Subtitle, CoverURL: card.CoverURL, DisplaySlot: card.DisplaySlot, DisplayPriority: card.DisplayPriority, HTMLURL: card.HTMLURL, } if card.EventPublicID != nil || card.EventDisplayName != nil { item.Event = &struct { ID string `json:"id"` DisplayName string `json:"displayName"` Summary *string `json:"summary,omitempty"` }{ Summary: card.EventSummary, } if card.EventPublicID != nil { item.Event.ID = *card.EventPublicID } if card.EventDisplayName != nil { item.Event.DisplayName = *card.EventDisplayName } } results = append(results, item) } return results }