package httpapi import ( "net/http" "strconv" "time" "github.com/descrybe/descrybe-v2/apps/api/internal/catalog" "github.com/descrybe/descrybe-v2/apps/api/internal/marketing" ) func (s *Server) marketingService() *marketing.Service { return &marketing.Service{Pool: s.Pool, Feeds: s.Feeds} } func (s *Server) handleGetMarketingCalendar(w http.ResponseWriter, r *http.Request) { cid, _ := CompanyIDFromContext(r.Context()) year := time.Now().UTC().Year() if y := r.URL.Query().Get("year"); y != "" { parsed, err := strconv.Atoi(y) if err != nil || parsed < 2000 || parsed > 2100 { Error(w, http.StatusBadRequest, "invalid year") return } year = parsed } prepared, err := s.marketingService().ListPreparedCampaigns(r.Context(), cid) if err != nil { Error(w, http.StatusInternalServerError, "list failed") return } JSON(w, http.StatusOK, map[string]any{ "year": year, "presets": marketing.ListPresets(year), "prepared": prepared, }) } func (s *Server) handlePrepareMarketingCalendar(w http.ResponseWriter, r *http.Request) { cid, _ := CompanyIDFromContext(r.Context()) var body struct { PresetID string `json:"preset_id"` Year int `json:"year"` Format string `json:"format"` ForceNew bool `json:"force_new"` } if err := DecodeJSON(r, &body); err != nil { Error(w, http.StatusBadRequest, "invalid json") return } campaign, err := s.marketingService().PrepareCampaign(r.Context(), cid, marketing.PrepareInput{ PresetID: marketing.PresetID(body.PresetID), Year: body.Year, Format: body.Format, ForceNew: body.ForceNew, }) if err != nil { ClientOrLog(w, http.StatusBadRequest, "could not prepare campaign", err, marketing.ClientError) return } status := http.StatusOK if campaign.Created { status = http.StatusCreated } JSON(w, status, campaign) } func (s *Server) handleListProductQuality(w http.ResponseWriter, r *http.Request) { cid, _ := CompanyIDFromContext(r.Context()) limit, offset := ParseLimitOffset(r) var minScore *int if raw := r.URL.Query().Get("min_score"); raw != "" { n, err := strconv.Atoi(raw) if err != nil { Error(w, http.StatusBadRequest, "invalid min_score") return } minScore = &n } f := catalog.ListFilter{ Query: QuerySearch(r), Status: r.URL.Query().Get("status"), Category: r.URL.Query().Get("category"), FeedID: firstNonEmpty(r.URL.Query().Get("feed_id"), r.URL.Query().Get("feedId")), Limit: limit, Offset: offset, } if f.Status == "" { f.Status = "completed" } items, total, err := s.Catalog.ListProcessedProductsDetailed(r.Context(), cid, f) if err != nil { Error(w, http.StatusInternalServerError, "list failed") return } out := make([]map[string]any, 0, len(items)) for _, item := range items { q := marketing.ScoreFromProductMap(item) if minScore != nil && q.Score < *minScore { continue } out = append(out, map[string]any{ "id": item["id"], "product_id": item["product_id"], "name": firstNonEmpty(asMapString(item["processed_name"]), asMapString(item["name"])), "quality_score": q.Score, "quality_grade": q.Grade, "quality_checks": q.Checks, }) } JSON(w, http.StatusOK, map[string]any{ "products": out, "total": total, "limit": limit, "offset": offset, }) } func asMapString(v any) string { if s, ok := v.(string); ok { return s } return "" } func attachProductQuality(items []map[string]any) { for i := range items { q := marketing.ScoreFromProductMap(items[i]) items[i]["quality_score"] = q.Score items[i]["quality_grade"] = q.Grade items[i]["quality_checks"] = q.Checks // Drop heavy fields used only for scoring when present on list payloads. delete(items[i], "mapped_data") delete(items[i], "attributes") delete(items[i], "processed_attributes") delete(items[i], "description") delete(items[i], "processed_description") delete(items[i], "meta_title") delete(items[i], "meta_description") } }