Initial commit of Descrybe v2 without local scratch artifacts.
Drop one-shot tmp/axe scripts and agent i18n scratch so the Gitea tree is deployable.
This commit is contained in:
@@ -0,0 +1,183 @@
|
||||
package httpapi
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
|
||||
"github.com/descrybe/descrybe-v2/apps/api/internal/billing"
|
||||
"github.com/descrybe/descrybe-v2/apps/api/internal/processing"
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
// startProcessingJobRequest is the SPA/API body for POST /processing/jobs.
|
||||
// ProcessingTypes is accepted because the dashboard sends fine-grained types
|
||||
// alongside the coarse ProcessingType used by StartJob; DecodeJSON rejects unknowns.
|
||||
type startProcessingJobRequest struct {
|
||||
RawProductIDs []string `json:"raw_product_ids"`
|
||||
ProcessingType string `json:"processing_type"`
|
||||
ProcessingTypes []string `json:"processing_types"`
|
||||
}
|
||||
|
||||
func (s *Server) handleStartProcessingJob(w http.ResponseWriter, r *http.Request) {
|
||||
cid, ok := CompanyIDFromContext(r.Context())
|
||||
if !ok || cid == uuid.Nil {
|
||||
Error(w, http.StatusUnauthorized, "unauthorized")
|
||||
return
|
||||
}
|
||||
uid, _ := UserIDFromContext(r.Context())
|
||||
var body startProcessingJobRequest
|
||||
if err := DecodeJSON(r, &body); err != nil {
|
||||
Error(w, http.StatusBadRequest, "invalid json")
|
||||
return
|
||||
}
|
||||
ids := make([]uuid.UUID, 0, len(body.RawProductIDs))
|
||||
for _, sID := range body.RawProductIDs {
|
||||
id, err := uuid.Parse(sID)
|
||||
if err != nil {
|
||||
Error(w, http.StatusBadRequest, "invalid raw_product_id")
|
||||
return
|
||||
}
|
||||
ids = append(ids, id)
|
||||
}
|
||||
jobs, err := s.Processing.StartJob(r.Context(), cid, uid, ids, body.ProcessingType)
|
||||
if err != nil {
|
||||
if writePlanGate(w, err) {
|
||||
return
|
||||
}
|
||||
if errors.Is(err, processing.ErrRateLimited) {
|
||||
Error(w, http.StatusTooManyRequests, err.Error())
|
||||
return
|
||||
}
|
||||
if msg, ok := processing.ClientError(err); ok {
|
||||
Error(w, http.StatusBadRequest, msg)
|
||||
return
|
||||
}
|
||||
LogAndError(w, http.StatusBadRequest, "could not start processing job", err)
|
||||
return
|
||||
}
|
||||
for _, job := range jobs {
|
||||
if err := s.Jobs.EnqueueProcessingJob(r.Context(), job.ID); err != nil {
|
||||
Error(w, http.StatusInternalServerError, "enqueue failed")
|
||||
return
|
||||
}
|
||||
}
|
||||
JSON(w, http.StatusAccepted, processing.FormatStartJobsResponse(jobs))
|
||||
}
|
||||
|
||||
func planGateCode(err error) string {
|
||||
switch {
|
||||
case errors.Is(err, billing.ErrInsufficientCredits):
|
||||
return "insufficient_credits"
|
||||
case errors.Is(err, billing.ErrProductLimitExceeded):
|
||||
return "product_limit"
|
||||
case errors.Is(err, billing.ErrAIRequiresUpgrade):
|
||||
return "ai_requires_upgrade"
|
||||
case errors.Is(err, billing.ErrEPRELRequiresUpgrade):
|
||||
return "eprel_requires_upgrade"
|
||||
case errors.Is(err, billing.ErrFeatureDisabled):
|
||||
return "plan_gate"
|
||||
default:
|
||||
return "plan_gate"
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) handleListProcessingJobs(w http.ResponseWriter, r *http.Request) {
|
||||
cid, ok := CompanyIDFromContext(r.Context())
|
||||
if !ok || cid == uuid.Nil {
|
||||
Error(w, http.StatusUnauthorized, "unauthorized")
|
||||
return
|
||||
}
|
||||
limit, _ := ParseLimitOffset(r)
|
||||
items, err := s.Processing.ListJobs(r.Context(), cid, limit)
|
||||
if err != nil {
|
||||
Error(w, http.StatusInternalServerError, "list failed")
|
||||
return
|
||||
}
|
||||
JSON(w, http.StatusOK, map[string]any{"jobs": processing.FormatListJobsResponse(items), "limit": limit})
|
||||
}
|
||||
|
||||
func (s *Server) handleGetProcessingJob(w http.ResponseWriter, r *http.Request) {
|
||||
cid, ok := CompanyIDFromContext(r.Context())
|
||||
if !ok || cid == uuid.Nil {
|
||||
Error(w, http.StatusUnauthorized, "unauthorized")
|
||||
return
|
||||
}
|
||||
id, err := uuid.Parse(chi.URLParam(r, "id"))
|
||||
if err != nil {
|
||||
Error(w, http.StatusBadRequest, "invalid id")
|
||||
return
|
||||
}
|
||||
job, err := s.getV1ProcessJob(r.Context(), cid, id)
|
||||
if err != nil {
|
||||
Error(w, http.StatusNotFound, "not found")
|
||||
return
|
||||
}
|
||||
if processing.JobStatusIncludesProducts(job.Status) {
|
||||
items, loadErr := s.loadV1ProcessJobItems(r.Context(), cid, id, job.ProcessingType)
|
||||
if loadErr != nil {
|
||||
Error(w, http.StatusInternalServerError, "load failed")
|
||||
return
|
||||
}
|
||||
JSON(w, http.StatusOK, processing.FormatJobStatusResponse(job, items, true))
|
||||
return
|
||||
}
|
||||
JSON(w, http.StatusOK, job)
|
||||
}
|
||||
|
||||
func (s *Server) handleCancelProcessingJob(w http.ResponseWriter, r *http.Request) {
|
||||
cid, ok := CompanyIDFromContext(r.Context())
|
||||
if !ok || cid == uuid.Nil {
|
||||
Error(w, http.StatusUnauthorized, "unauthorized")
|
||||
return
|
||||
}
|
||||
id, err := uuid.Parse(chi.URLParam(r, "id"))
|
||||
if err != nil {
|
||||
Error(w, http.StatusBadRequest, "invalid id")
|
||||
return
|
||||
}
|
||||
job, err := s.Processing.CancelJob(r.Context(), cid, id)
|
||||
if err != nil {
|
||||
if msg, ok := processing.ClientError(err); ok {
|
||||
Error(w, http.StatusBadRequest, msg)
|
||||
return
|
||||
}
|
||||
LogAndError(w, http.StatusBadRequest, "could not cancel job", err)
|
||||
return
|
||||
}
|
||||
JSON(w, http.StatusOK, job)
|
||||
}
|
||||
|
||||
func (s *Server) handleRetryProcessingJob(w http.ResponseWriter, r *http.Request) {
|
||||
cid, ok := CompanyIDFromContext(r.Context())
|
||||
if !ok || cid == uuid.Nil {
|
||||
Error(w, http.StatusUnauthorized, "unauthorized")
|
||||
return
|
||||
}
|
||||
id, err := uuid.Parse(chi.URLParam(r, "id"))
|
||||
if err != nil {
|
||||
Error(w, http.StatusBadRequest, "invalid id")
|
||||
return
|
||||
}
|
||||
job, err := s.Processing.RetryJob(r.Context(), cid, id)
|
||||
if err != nil {
|
||||
if writePlanGate(w, err) {
|
||||
return
|
||||
}
|
||||
if errors.Is(err, processing.ErrRateLimited) {
|
||||
Error(w, http.StatusTooManyRequests, err.Error())
|
||||
return
|
||||
}
|
||||
if msg, ok := processing.ClientError(err); ok {
|
||||
Error(w, http.StatusBadRequest, msg)
|
||||
return
|
||||
}
|
||||
LogAndError(w, http.StatusBadRequest, "could not retry job", err)
|
||||
return
|
||||
}
|
||||
if err := s.Jobs.EnqueueProcessingJob(r.Context(), job.ID); err != nil {
|
||||
Error(w, http.StatusInternalServerError, "enqueue failed")
|
||||
return
|
||||
}
|
||||
JSON(w, http.StatusAccepted, job)
|
||||
}
|
||||
Reference in New Issue
Block a user