package productDescriptionService import ( "encoding/xml" "fmt" "io" "slices" "strings" "git.ma-al.com/goc_daniel/b2b/app/db" "git.ma-al.com/goc_daniel/b2b/app/model" "git.ma-al.com/goc_daniel/b2b/app/utils/responseErrors" "gorm.io/gorm" ) type ProductDescriptionService struct { db *gorm.DB } func New() *ProductDescriptionService { return &ProductDescriptionService{ db: db.Get(), } } func isValidXHTML(s string) bool { decoder := xml.NewDecoder(strings.NewReader(s)) hasStartTag := false for { tok, err := decoder.Token() if err != nil { if err == io.EOF { return hasStartTag } return false } if _, ok := tok.(xml.StartElement); ok { hasStartTag = true } } } // We assume that any user has access to all product descriptions func (s *ProductDescriptionService) GetProductDescription(userID uint, productID uint, productShopID uint, productLangID uint) (*model.ProductDescription, error) { var ProductDescription model.ProductDescription err := s.db. Table("ps_product_lang"). Where("id_product = ? AND id_shop = ? AND id_lang = ?", productID, productShopID, productLangID). First(&ProductDescription).Error if err != nil { return nil, fmt.Errorf("database error: %w", err) } return &ProductDescription, nil } // Updates relevant fields with the "updates" map func (s *ProductDescriptionService) SaveProductDescription(userID uint, productID uint, productShopID uint, productLangID uint, updates map[string]string) error { // only some fields can be affected allowedFields := []string{"description", "description_short", "meta_description", "meta_title", "name", "available_now", "available_later", "usage"} for key := range updates { if !slices.Contains(allowedFields, key) { return responseErrors.ErrBadField } } // check that fields description, description_short and usage, if they exist, have a valid html format mustBeHTML := []string{"description", "description_short", "usage"} for i := 0; i < len(mustBeHTML); i++ { if text, exists := updates[mustBeHTML[i]]; exists { if !isValidXHTML(text) { return responseErrors.ErrInvalidHTML } } } record := model.ProductDescription{ ProductID: productID, ShopID: productShopID, LangID: productLangID, } err := s.db. Table("ps_product_lang"). Where("id_product = ? AND id_shop = ? AND id_lang = ?", productID, productShopID, productLangID). FirstOrCreate(&record).Error if err != nil { return fmt.Errorf("database error: %w", err) } if len(updates) == 0 { return nil } updatesIface := make(map[string]interface{}, len(updates)) for k, v := range updates { updatesIface[k] = v } err = s.db. Table("ps_product_lang"). Where("id_product = ? AND id_shop = ? AND id_lang = ?", productID, productShopID, productLangID). Updates(updatesIface).Error if err != nil { return fmt.Errorf("database error: %w", err) } return nil } // func (s *ProductDescriptionService) GetRepositoriesForUser(userID uint) ([]uint, error) { // var repoIDs []uint // err := s.db. // Table("customer_repo_accesses"). // Where("user_id = ?", userID). // Pluck("repo_id", &repoIDs).Error // if err != nil { // return nil, fmt.Errorf("database error: %w", err) // } // return repoIDs, nil // } // func (s *RepoService) UserHasAccessToRepo(userID uint, repoID uint) (bool, error) { // var repositories []uint // var err error // if repositories, err = s.GetRepositoriesForUser(userID); err != nil { // return false, err // } // if !slices.Contains(repositories, repoID) { // return false, responseErrors.ErrInvalidRepoID // } // return true, nil // } // // Extract all repositories assigned to user with specific id // func (s *RepoService) GetYearsForUser(userID uint, repoID uint) ([]uint, error) { // if ok, err := s.UserHasAccessToRepo(userID, repoID); !ok { // return nil, err // } // years, err := s.GetYears(repoID) // if err != nil { // return nil, fmt.Errorf("database error: %w", err) // } // return years, nil // } // func (s *RepoService) GetYears(repo uint) ([]uint, error) { // var years []uint // query := ` // WITH bounds AS ( // SELECT // MIN(to_timestamp(tt.created_unix)) AS min_ts, // MAX(to_timestamp(tt.created_unix)) AS max_ts // FROM tracked_time tt // JOIN issue i ON i.id = tt.issue_id // WHERE i.repo_id = ? // AND tt.deleted = false // ) // SELECT // EXTRACT(YEAR FROM y.year_start)::int AS year // FROM bounds // CROSS JOIN LATERAL generate_series( // date_trunc('year', min_ts), // date_trunc('year', max_ts), // interval '1 year' // ) AS y(year_start) // ORDER BY year // ` // err := db.Get().Raw(query, repo).Find(&years).Error // if err != nil { // return nil, err // } // return years, nil // } // // Extract all repositories assigned to user with specific id // func (s *RepoService) GetQuartersForUser(userID uint, repoID uint, year uint) ([]model.QuarterData, error) { // if ok, err := s.UserHasAccessToRepo(userID, repoID); !ok { // return nil, err // } // response, err := s.GetQuarters(repoID, year) // if err != nil { // return nil, fmt.Errorf("database error: %w", err) // } // return response, nil // } // func (s *RepoService) GetQuarters(repo uint, year uint) ([]model.QuarterData, error) { // var quarters []model.QuarterData // query := ` // WITH quarters AS ( // SELECT // make_date(?::int, 1, 1) + (q * interval '3 months') AS quarter_start, // q + 1 AS quarter // FROM generate_series(0, 3) AS q // ), // data AS ( // SELECT // EXTRACT(QUARTER FROM to_timestamp(tt.created_unix)) AS quarter, // SUM(tt.time) / 3600 AS time // FROM tracked_time tt // JOIN issue i ON i.id = tt.issue_id // JOIN repository r ON i.repo_id = r.id // WHERE // EXTRACT(YEAR FROM to_timestamp(tt.created_unix)) = ? // AND r.id = ? // AND tt.deleted = false // GROUP BY EXTRACT(QUARTER FROM to_timestamp(tt.created_unix)) // ) // SELECT // COALESCE(d.time, 0) AS time, // CONCAT(EXTRACT(YEAR FROM q.quarter_start)::int, '_Q', q.quarter) AS quarter // FROM quarters q // LEFT JOIN data d ON d.quarter = q.quarter // ORDER BY q.quarter // ` // err := db.Get(). // Raw(query, year, year, repo). // Find(&quarters). // Error // if err != nil { // return nil, err // } // return quarters, nil // } // func (s *RepoService) GetIssuesForUser( // userID uint, // repoID uint, // year uint, // quarter uint, // p pagination.Paging, // ) (*pagination.Found[model.IssueTimeSummary], error) { // if ok, err := s.UserHasAccessToRepo(userID, repoID); !ok { // return nil, err // } // return s.GetIssues(repoID, year, quarter, p) // } // func (s *RepoService) GetIssues( // repoId uint, // year uint, // quarter uint, // p pagination.Paging, // ) (*pagination.Found[model.IssueTimeSummary], error) { // query := db.Get(). // Table("issue i"). // Select(` // i.id AS issue_id, // i.name AS issue_name, // to_timestamp(i.created_unix) AS issue_created_at, // ROUND(SUM(tt.time) / 3600.0, 2) AS total_hours_spent // `). // Joins(`JOIN tracked_time tt ON tt.issue_id = i.id`). // Joins(`JOIN "user" u ON u.id = tt.user_id`). // Where("i.repo_id = ?", repoId). // Where(` // EXTRACT(YEAR FROM to_timestamp(tt.created_unix)) = ? // AND EXTRACT(QUARTER FROM to_timestamp(tt.created_unix)) = ? // `, year, quarter). // Group(` // i.id, // i.name, // u.id, // u.full_name // `). // Order("i.created_unix") // result, err := pagination.Paginate[model.IssueTimeSummary](p, query) // if err != nil { // return nil, err // } // return &result, nil // }