|
- package draftsvc
-
- import (
- "context"
- "encoding/json"
- "errors"
- "fmt"
- "strconv"
- "strings"
- "time"
-
- "qctextbuilder/internal/domain"
- "qctextbuilder/internal/store"
- )
-
- type UpsertDraftRequest struct {
- DraftID string `json:"draftId,omitempty"`
- TemplateID *int64 `json:"templateId,omitempty"`
- ManifestID string `json:"manifestId,omitempty"`
- Source string `json:"source,omitempty"`
- RequestName string `json:"requestName,omitempty"`
- GlobalData map[string]any `json:"globalData"`
- FieldValues map[string]string `json:"fieldValues"`
- DraftContext *domain.DraftContext `json:"draftContext,omitempty"`
- SuggestionState *domain.DraftSuggestionState `json:"suggestionState,omitempty"`
- Status string `json:"status,omitempty"`
- Notes string `json:"notes,omitempty"`
- }
-
- type Service struct {
- drafts store.DraftStore
- templates store.TemplateStore
- manifests store.ManifestStore
- }
-
- func New(draftStore store.DraftStore, templateStore store.TemplateStore, manifestStore store.ManifestStore) *Service {
- return &Service{
- drafts: draftStore,
- templates: templateStore,
- manifests: manifestStore,
- }
- }
-
- func (s *Service) SaveDraft(ctx context.Context, req UpsertDraftRequest) (*domain.BuildDraft, error) {
- templateID := int64(0)
- if req.TemplateID != nil {
- templateID = *req.TemplateID
- }
- var existing *domain.BuildDraft
- if strings.TrimSpace(req.DraftID) != "" {
- var err error
- existing, err = s.drafts.GetDraftByID(ctx, strings.TrimSpace(req.DraftID))
- if err != nil {
- return nil, fmt.Errorf("get draft: %w", err)
- }
- if templateID == 0 {
- templateID = existing.TemplateID
- }
- }
-
- if templateID > 0 {
- template, err := s.templates.GetTemplateByID(ctx, templateID)
- if err != nil {
- return nil, fmt.Errorf("get template: %w", err)
- }
- if !template.IsAITemplate {
- return nil, errors.New("only ai templates are allowed")
- }
- }
-
- manifestID := strings.TrimSpace(req.ManifestID)
- if templateID <= 0 {
- manifestID = ""
- } else if manifestID == "" {
- manifest, err := s.manifests.GetActiveManifestByTemplateID(ctx, templateID)
- if err != nil {
- return nil, fmt.Errorf("get active manifest: %w", err)
- }
- manifestID = manifest.ID
- }
-
- globalDataJSON, err := json.Marshal(req.GlobalData)
- if err != nil {
- return nil, errors.New("globalData is invalid JSON")
- }
- fieldValuesJSON, err := json.Marshal(req.FieldValues)
- if err != nil {
- return nil, errors.New("fieldValues is invalid JSON")
- }
- draftContextJSON, err := buildDraftContextJSON(req, existing)
- if err != nil {
- return nil, err
- }
- suggestionStateJSON, err := buildSuggestionStateJSON(req, existing)
- if err != nil {
- return nil, err
- }
-
- now := time.Now().UTC()
- source := defaultString(req.Source, "ui")
- status := normalizeDraftStatus(req.Status)
- if status == "" {
- status = "draft"
- }
-
- draft := domain.BuildDraft{
- ID: strings.TrimSpace(req.DraftID),
- TemplateID: templateID,
- ManifestID: manifestID,
- Source: source,
- RequestName: strings.TrimSpace(req.RequestName),
- GlobalDataJSON: globalDataJSON,
- FieldValuesJSON: fieldValuesJSON,
- DraftContextJSON: draftContextJSON,
- SuggestionStateJSON: suggestionStateJSON,
- Status: status,
- Notes: strings.TrimSpace(req.Notes),
- UpdatedAt: now,
- }
- if draft.ID == "" {
- draft.ID = strconv.FormatInt(time.Now().UnixNano(), 10)
- draft.CreatedAt = now
- if err := s.drafts.CreateDraft(ctx, draft); err != nil {
- return nil, fmt.Errorf("create draft: %w", err)
- }
- } else {
- existing, err := s.drafts.GetDraftByID(ctx, draft.ID)
- if err != nil {
- return nil, fmt.Errorf("get draft: %w", err)
- }
- draft.CreatedAt = existing.CreatedAt
- if err := s.drafts.UpdateDraft(ctx, draft); err != nil {
- return nil, fmt.Errorf("update draft: %w", err)
- }
- }
-
- return s.drafts.GetDraftByID(ctx, draft.ID)
- }
-
- func buildDraftContextJSON(req UpsertDraftRequest, existing *domain.BuildDraft) (json.RawMessage, error) {
- if req.DraftContext == nil {
- if existing != nil {
- return existing.DraftContextJSON, nil
- }
- return nil, nil
- }
- raw, err := json.Marshal(req.DraftContext)
- if err != nil {
- return nil, errors.New("draftContext is invalid JSON")
- }
- return raw, nil
- }
-
- func buildSuggestionStateJSON(req UpsertDraftRequest, existing *domain.BuildDraft) (json.RawMessage, error) {
- if req.SuggestionState == nil {
- if existing != nil {
- return existing.SuggestionStateJSON, nil
- }
- return nil, nil
- }
- raw, err := json.Marshal(req.SuggestionState)
- if err != nil {
- return nil, errors.New("suggestionState is invalid JSON")
- }
- return raw, nil
- }
-
- func (s *Service) GetDraft(ctx context.Context, draftID string) (*domain.BuildDraft, error) {
- return s.drafts.GetDraftByID(ctx, strings.TrimSpace(draftID))
- }
-
- func (s *Service) ListDrafts(ctx context.Context, limit int) ([]domain.BuildDraft, error) {
- if limit <= 0 {
- limit = 50
- }
- return s.drafts.ListDrafts(ctx, limit)
- }
-
- func normalizeDraftStatus(status string) string {
- switch strings.ToLower(strings.TrimSpace(status)) {
- case "":
- return ""
- case "draft", "reviewed", "submitted":
- return strings.ToLower(strings.TrimSpace(status))
- default:
- return "draft"
- }
- }
-
- func defaultString(value, fallback string) string {
- if strings.TrimSpace(value) == "" {
- return fallback
- }
- return strings.TrimSpace(value)
- }
|