package discovery import ( "context" "strings" "time" "github.com/google/uuid" "github.com/rs/zerolog" "go.uber.org/fx" "golang.org/x/sync/errgroup" domainAsset "base/internal/domain/asset" domainProfile "base/internal/domain/profile" "base/internal/dto" ) type Service interface { GetDiscoveryOverview(ctx context.Context) (*dto.OverviewFetchedResponse, error) } type service struct { logger zerolog.Logger profileRepo domainProfile.Repository assetRepo domainAsset.AssetRepository categoryRepo domainAsset.CategoryRepository } // Param holds dependencies for the discovery overview service. type Param struct { Logger zerolog.Logger ProfileRepo domainProfile.Repository AssetRepo domainAsset.AssetRepository CategoryRepo domainAsset.CategoryRepository fx.In } func New(param Param) Service { return &service{ logger: param.Logger, profileRepo: param.ProfileRepo, assetRepo: param.AssetRepo, categoryRepo: param.CategoryRepo, } } func (s *service) GetDiscoveryOverview(ctx context.Context) (*dto.OverviewFetchedResponse, error) { resp := &struct { domainAssets []*domainAsset.Asset recentlyJoined []*domainProfile.Profile totalProfiles int totalAssets int }{} g, gCtx := errgroup.WithContext(ctx) g.Go(func() error { assets, err := s.assetRepo.FindLatest(gCtx, 6, 0) if err != nil { return err } resp.domainAssets = assets return nil }) g.Go(func() error { profiles, total, err := s.profileRepo.FindAll(gCtx, domainProfile.Filter{ Page: 1, PageSize: 6, SortedBy: "created_at", Ascending: false, }) if err != nil { return err } resp.recentlyJoined = profiles resp.totalProfiles = total return nil }) g.Go(func() error { count, err := s.assetRepo.Count(gCtx) if err != nil { return err } resp.totalAssets = count return nil }) if err := g.Wait(); err != nil { return nil, err } assets := s.toOverviewAssets(ctx, resp.domainAssets) flatProfiles := ToFlatProfiles(resp.recentlyJoined) return &dto.OverviewFetchedResponse{ Message: "Overview fetched successfully", Data: dto.OverviewFetchedDataDTO{ Assets: assets, RecentlyJoined: flatProfiles, Analytics: dto.AnalyticsDTO{ TotalAssets: resp.totalAssets, TotalProfiles: resp.totalProfiles, }, }, }, nil } func (s *service) toOverviewAssets(ctx context.Context, assets []*domainAsset.Asset) []dto.OverviewAssetDTO { out := make([]dto.OverviewAssetDTO, len(assets)) for i, a := range assets { out[i] = s.toOverviewAsset(ctx, a) } return out } func (s *service) toOverviewAsset(ctx context.Context, a *domainAsset.Asset) dto.OverviewAssetDTO { price := 0 coverImage := "" for _, art := range a.AssetArtifacts { if strings.Contains(strings.ToLower(art.Type), "image") { coverImage = art.DownloadURL break } } if len(a.AssetArtifacts) > 0 { price = a.AssetArtifacts[0].Price } cat := (*dto.CategoryDTO)(nil) if a.AssetCategory.ID != uuid.Nil { cat = &dto.CategoryDTO{ ID: a.AssetCategory.ID, Name: a.AssetCategory.Name, Icon: a.AssetCategory.Icon, Color: a.AssetCategory.Color, CardType: a.AssetCategory.CardType, Featured: a.AssetCategory.Featured, Description: a.AssetCategory.Description, } } ownerID := a.ProfileID.String() if p, err := s.profileRepo.FindByID(ctx, a.ProfileID); err == nil && p.UserID != nil { ownerID = p.UserID.String() } return dto.OverviewAssetDTO{ ID: a.ID.String(), Title: a.Title, Description: a.Description, Content: a.Description, AssetCategoryID: a.AssetCategoryID.String(), AssetCategory: cat, CoverImage: coverImage, Link: a.Link, OwnerID: ownerID, ProfileID: a.ProfileID.String(), Profile: nil, Price: price, Currency: "USD", Status: assetStatusToString(a.Status), Rating: 0, CreatedAt: a.CreatedAt, UpdatedAt: a.UpdatedAt, } } func assetStatusToString(st domainAsset.Status) string { switch st { case domainAsset.StatusPublished: return "published" case domainAsset.StatusDisabled: return "disabled" case domainAsset.StatusPending: return "pending" case domainAsset.StatusDeleted: return "deleted" default: return "" } } func formatTime(t time.Time) string { if t.IsZero() { return "" } return t.Format(time.RFC3339) } // ToFlatProfiles converts domain profiles to flat DTOs. func ToFlatProfiles(profiles []*domainProfile.Profile) []dto.FlatProfileDTO { out := make([]dto.FlatProfileDTO, len(profiles)) for i, p := range profiles { out[i] = ToFlatProfile(p) } return out } // ToFlatProfile converts a single profile to flat DTO. func ToFlatProfile(p *domainProfile.Profile) dto.FlatProfileDTO { roleID := "" roleName := "" if p.Hero.Role != nil { roleID = p.Hero.Role.ID.String() if p.Hero.Role.Title != "" { roleName = p.Hero.Role.Title } } achievements := make(map[string]dto.AchievementItemDTO) for _, a := range p.About.Achievements { key := strings.ToLower(strings.ReplaceAll(a.Title, " ", "")) if key == "" { key = a.Title } achievements[key] = dto.AchievementItemDTO{Value: a.Value, Enabled: a.Enabled} } if len(achievements) == 0 { achievements = map[string]dto.AchievementItemDTO{ "happyClient": {Value: "", Enabled: true}, "yearExperience": {Value: "", Enabled: true}, "projectCompeleted": {Value: "", Enabled: true}, } } var socialLinks []dto.SocialLinkDTO for _, sl := range p.Contact.SocialLinks { socialLinks = append(socialLinks, dto.SocialLinkDTO{LinkType: sl.LinkType, Link: sl.Link}) } displayName := strings.TrimSpace(p.Hero.FirstName + " " + p.Hero.LastName) if displayName == "" { displayName = p.Handle } status := "published" if p.PageSetting.VisibilityLevel != "public" { status = "draft" } return dto.FlatProfileDTO{ ID: p.ID.String(), ProfileHandle: p.Handle, Status: status, BackgroundImage: "", ProfilePicture: p.About.ProfilePicture, FirstName: p.Hero.FirstName, LastName: p.Hero.LastName, DisplayName: displayName, RoleID: roleID, Role: dto.RoleDTO{ID: roleID, Name: roleName}, CurrentCompany: p.Hero.Company, ShortDescription: p.Hero.ShortDescription, CTAEnabled: p.Hero.CTAEnabled, CTAAction: "", ResumeLink: p.Hero.ResumeLink, About: p.About.About, ContactEmail: p.Contact.Email, Achievements: achievements, ContactPhone: p.Contact.Phone, Country: "", CustomRoles: "", RoleLevel: p.Hero.Role.Level, SocialLinks: socialLinks, CreatedAt: p.CreatedAt, UpdatedAt: p.UpdatedAt, HandleUpdatedAt: time.Time{}, } }