initial commit

This commit is contained in:
m.zare
2026-04-10 18:25:21 +03:30
commit 77ca6c34a3
263 changed files with 34470 additions and 0 deletions

View File

@@ -0,0 +1,349 @@
package asset
import (
"context"
"errors"
"strings"
"time"
"github.com/google/uuid"
"github.com/rs/zerolog"
"go.uber.org/fx"
"golang.org/x/sync/errgroup"
domainAsset "base/internal/domain/asset"
"base/internal/dto"
)
var (
ErrAssetNotFound = errors.New("asset not found")
ErrCategoryNotFound = errors.New("asset category not found")
)
type Service interface {
Create(ctx context.Context, req dto.CreateAssetRequest) (*dto.AssetResponse, error)
GetByID(ctx context.Context, id uuid.UUID) (*dto.AssetResponse, error)
Update(ctx context.Context, req dto.UpdateAssetRequest) (*dto.AssetResponse, error)
Delete(ctx context.Context, id uuid.UUID) error
FindByProfileID(ctx context.Context, profileID uuid.UUID) (*dto.ListAssetsResponse, error)
ListCategories(ctx context.Context) (*dto.ListCategoriesResponse, error)
ListByCategoryID(ctx context.Context, categoryID uuid.UUID, limit, page int) (*dto.ListAssetsByCategoryIDResponse, error)
GetCategoriesWithPreview(ctx context.Context, req dto.CategoriesPreviewRequest) (*dto.CategoriesPreviewResponse, error)
}
type service struct {
logger zerolog.Logger
assetRepo domainAsset.AssetRepository
categoryRepo domainAsset.CategoryRepository
}
type Param struct {
Logger zerolog.Logger
AssetRepo domainAsset.AssetRepository
CategoryRepo domainAsset.CategoryRepository
fx.In
}
func New(param Param) Service {
return &service{
logger: param.Logger,
assetRepo: param.AssetRepo,
categoryRepo: param.CategoryRepo,
}
}
func (s *service) Create(ctx context.Context, req dto.CreateAssetRequest) (*dto.AssetResponse, error) {
profileID, err := uuid.Parse(req.ProfileID)
if err != nil {
return nil, ErrAssetNotFound
}
categoryID, err := uuid.Parse(req.AssetCategoryID)
if err != nil {
return nil, ErrCategoryNotFound
}
// Verify category exists
category, err := s.categoryRepo.FindByID(ctx, categoryID)
if err != nil || category == nil {
return nil, ErrCategoryNotFound
}
asset := &domainAsset.Asset{
ID: uuid.New(),
ProfileID: profileID,
AssetCategoryID: categoryID,
AssetCategory: *category,
Title: req.Title,
Description: req.Description,
Link: req.Link,
Status: domainAsset.StatusPublished,
}
if err := s.assetRepo.Create(ctx, asset); err != nil {
return nil, err
}
return s.toAssetResponse(asset), nil
}
func (s *service) GetByID(ctx context.Context, id uuid.UUID) (*dto.AssetResponse, error) {
asset, err := s.assetRepo.FindByID(ctx, id)
if err != nil {
return nil, ErrAssetNotFound
}
return s.toAssetResponse(asset), nil
}
func (s *service) Update(ctx context.Context, req dto.UpdateAssetRequest) (*dto.AssetResponse, error) {
id, err := uuid.Parse(req.ID)
if err != nil {
return nil, ErrAssetNotFound
}
asset, err := s.assetRepo.FindByID(ctx, id)
if err != nil {
return nil, ErrAssetNotFound
}
asset.Title = req.Title
asset.Description = req.Description
asset.Link = req.Link
if req.AssetCategoryID != "" {
categoryID, err := uuid.Parse(req.AssetCategoryID)
if err == nil {
category, err := s.categoryRepo.FindByID(ctx, categoryID)
if err == nil && category != nil {
asset.AssetCategoryID = categoryID
asset.AssetCategory = *category
}
}
}
if req.Status != nil && *req.Status >= 0 && *req.Status <= 3 {
asset.Status = domainAsset.Status(*req.Status)
}
if err := s.assetRepo.Update(ctx, asset); err != nil {
return nil, err
}
return s.toAssetResponse(asset), nil
}
func (s *service) Delete(ctx context.Context, id uuid.UUID) error {
asset, err := s.assetRepo.FindByID(ctx, id)
if err != nil {
return ErrAssetNotFound
}
return s.assetRepo.Delete(ctx, asset)
}
func (s *service) FindByProfileID(ctx context.Context, profileID uuid.UUID) (*dto.ListAssetsResponse, error) {
assets, err := s.assetRepo.FindByProfileID(ctx, profileID)
if err != nil {
return nil, err
}
resp := &dto.ListAssetsResponse{
Assets: make([]dto.AssetResponse, len(assets)),
}
for i, a := range assets {
resp.Assets[i] = *s.toAssetResponse(a)
}
return resp, nil
}
func (s *service) ListCategories(ctx context.Context) (*dto.ListCategoriesResponse, error) {
categories, err := s.categoryRepo.FindAll(ctx)
if err != nil {
return nil, err
}
resp := &dto.ListCategoriesResponse{
Categories: make([]dto.CategoryDTO, len(categories)),
}
for i, c := range categories {
resp.Categories[i] = dto.CategoryDTO{
ID: c.ID,
Name: c.Name,
Icon: c.Icon,
Color: c.Color,
CardType: c.CardType,
Featured: c.Featured,
Description: c.Description,
}
}
return resp, nil
}
func (s *service) ListByCategoryID(ctx context.Context, categoryID uuid.UUID, limit, page int) (*dto.ListAssetsByCategoryIDResponse, error) {
if limit < 1 {
limit = 10
}
if page < 1 {
page = 1
}
category, err := s.categoryRepo.FindByID(ctx, categoryID)
if err != nil || category == nil {
return nil, ErrCategoryNotFound
}
total, err := s.assetRepo.CountByCategory(ctx, categoryID)
if err != nil {
return nil, err
}
offset := (page - 1) * limit
assets, err := s.assetRepo.FindLatestByCategoryPaginated(ctx, categoryID, limit, offset)
if err != nil {
return nil, err
}
totalPages := (total + limit - 1) / limit
if totalPages < 1 {
totalPages = 1
}
resp := &dto.ListAssetsByCategoryIDResponse{
Category: dto.CategoryDTO{
ID: category.ID,
Name: category.Name,
Icon: category.Icon,
Color: category.Color,
CardType: category.CardType,
Featured: category.Featured,
Description: category.Description,
},
Assets: make([]dto.AssetResponse, len(assets)),
Total: total,
Page: page,
PageSize: limit,
TotalPages: totalPages,
}
for i, a := range assets {
resp.Assets[i] = *s.toAssetResponse(a)
}
return resp, nil
}
func (s *service) GetCategoriesWithPreview(ctx context.Context, req dto.CategoriesPreviewRequest) (*dto.CategoriesPreviewResponse, error) {
perCategory := req.AssetsPerCategory
if perCategory < 1 {
perCategory = 8
}
if perCategory > 20 {
perCategory = 20
}
var categoryIDs []uuid.UUID
for _, s := range req.CategoryIDs {
if id, err := uuid.Parse(s); err == nil {
categoryIDs = append(categoryIDs, id)
}
}
var categories []*domainAsset.Category
var err error
if len(categoryIDs) > 0 {
categories, err = s.categoryRepo.FindByIDs(ctx, categoryIDs)
} else {
categories, err = s.categoryRepo.FindAll(ctx)
}
if err != nil {
return nil, err
}
if req.FeaturedOnly {
filtered := make([]*domainAsset.Category, 0, len(categories))
for _, c := range categories {
if c.Featured {
filtered = append(filtered, c)
}
}
categories = filtered
}
results := make([]dto.CategoryWithPreviewAssetsDTO, len(categories))
g, gCtx := errgroup.WithContext(ctx)
for index, category := range categories {
i, cat := index, category
g.Go(func() error {
assets, assetErr := s.assetRepo.FindLatestByCategory(gCtx, cat.ID, perCategory)
if assetErr != nil {
return assetErr
}
total, _ := s.assetRepo.CountByCategory(gCtx, cat.ID)
assetResps := make([]dto.AssetResponse, len(assets))
for j, a := range assets {
assetResps[j] = *s.toAssetResponse(a)
}
results[i] = dto.CategoryWithPreviewAssetsDTO{
Category: dto.CategoryDTO{
ID: cat.ID,
Name: cat.Name,
Icon: cat.Icon,
Color: cat.Color,
CardType: cat.CardType,
Featured: cat.Featured,
Description: cat.Description,
},
Assets: assetResps,
TotalAssets: total,
HasMore: total > perCategory,
}
return nil
})
}
if err := g.Wait(); err != nil {
return nil, err
}
return &dto.CategoriesPreviewResponse{Categories: results}, nil
}
func (s *service) toAssetResponse(a *domainAsset.Asset) *dto.AssetResponse {
coverImage := ""
for _, art := range a.AssetArtifacts {
if strings.Contains(strings.ToLower(art.Type), "image") {
coverImage = art.DownloadURL
break
}
}
resp := &dto.AssetResponse{
ID: a.ID,
ProfileID: a.ProfileID,
AssetCategoryID: a.AssetCategoryID,
Title: a.Title,
Description: a.Description,
Link: a.Link,
CoverImage: coverImage,
Status: int(a.Status),
CreatedAt: formatTime(a.CreatedAt),
UpdatedAt: formatTime(a.UpdatedAt),
}
resp.Category = 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,
}
return resp
}
func formatTime(t time.Time) string {
if t.IsZero() {
return ""
}
return t.Format(time.RFC3339)
}