package usecase import ( "context" "strings" "haixun-backend/internal/library/clock" app "haixun-backend/internal/library/errors" "haixun-backend/internal/library/errors/code" "haixun-backend/internal/model/copy_draft/domain/entity" domrepo "haixun-backend/internal/model/copy_draft/domain/repository" domusecase "haixun-backend/internal/model/copy_draft/domain/usecase" "github.com/google/uuid" ) type copyDraftUseCase struct { repo domrepo.Repository } func NewUseCase(repo domrepo.Repository) domusecase.UseCase { return ©DraftUseCase{repo: repo} } func (u *copyDraftUseCase) Create(ctx context.Context, req domusecase.CreateRequest) (*domusecase.CopyDraftSummary, error) { if err := requireActor(req.TenantID, req.OwnerUID, req.PersonaID); err != nil { return nil, err } text := strings.TrimSpace(req.Text) if text == "" { return nil, app.For(code.Persona).InputMissingRequired("draft text is required") } draftType := strings.TrimSpace(req.DraftType) if draftType == "" { draftType = entity.DraftTypeViralReplica } item := &entity.CopyDraft{ ID: uuid.NewString(), TenantID: req.TenantID, OwnerUID: req.OwnerUID, PersonaID: req.PersonaID, CopyMissionID: strings.TrimSpace(req.CopyMissionID), ScanPostID: strings.TrimSpace(req.ScanPostID), DraftType: draftType, SortOrder: req.SortOrder, Text: text, Angle: strings.TrimSpace(req.Angle), Hook: strings.TrimSpace(req.Hook), Rationale: strings.TrimSpace(req.Rationale), ReferenceNotes: strings.TrimSpace(req.ReferenceNotes), Sources: req.Sources, Status: "pending", CreateAt: clock.NowUnixNano(), } if err := u.repo.Create(ctx, item); err != nil { return nil, err } summary := toSummary(*item) return &summary, nil } func (u *copyDraftUseCase) CreateMany(ctx context.Context, req domusecase.CreateManyRequest) ([]domusecase.CopyDraftSummary, error) { if err := requireActor(req.TenantID, req.OwnerUID, req.PersonaID); err != nil { return nil, err } if len(req.Drafts) == 0 { return nil, app.For(code.Persona).InputMissingRequired("drafts is required") } items := make([]*entity.CopyDraft, 0, len(req.Drafts)) for idx, draftReq := range req.Drafts { text := strings.TrimSpace(draftReq.Text) if text == "" { continue } draftType := strings.TrimSpace(draftReq.DraftType) if draftType == "" { draftType = entity.DraftTypeMatrix } sortOrder := draftReq.SortOrder if sortOrder == 0 { sortOrder = idx + 1 } items = append(items, &entity.CopyDraft{ ID: uuid.NewString(), TenantID: req.TenantID, OwnerUID: req.OwnerUID, PersonaID: req.PersonaID, CopyMissionID: strings.TrimSpace(draftReq.CopyMissionID), ScanPostID: strings.TrimSpace(draftReq.ScanPostID), DraftType: draftType, SortOrder: sortOrder, Text: text, Angle: strings.TrimSpace(draftReq.Angle), Hook: strings.TrimSpace(draftReq.Hook), Rationale: strings.TrimSpace(draftReq.Rationale), ReferenceNotes: strings.TrimSpace(draftReq.ReferenceNotes), Sources: draftReq.Sources, Status: "pending", CreateAt: clock.NowUnixNano(), }) } if len(items) == 0 { return nil, app.For(code.Persona).InputMissingRequired("draft text is required") } if err := u.repo.CreateMany(ctx, items); err != nil { return nil, err } out := make([]domusecase.CopyDraftSummary, 0, len(items)) for _, item := range items { out = append(out, toSummary(*item)) } return out, nil } func (u *copyDraftUseCase) Get(ctx context.Context, tenantID, ownerUID, personaID, draftID string) (*domusecase.CopyDraftSummary, error) { if err := requireActor(tenantID, ownerUID, personaID); err != nil { return nil, err } item, err := u.repo.Get(ctx, tenantID, ownerUID, personaID, draftID) if err != nil { return nil, err } summary := toSummary(*item) return &summary, nil } func (u *copyDraftUseCase) Update(ctx context.Context, req domusecase.UpdateRequest) (*domusecase.CopyDraftSummary, error) { if err := requireActor(req.TenantID, req.OwnerUID, req.PersonaID); err != nil { return nil, err } draftID := strings.TrimSpace(req.DraftID) if draftID == "" { return nil, app.For(code.Persona).InputMissingRequired("draft_id is required") } patch := map[string]interface{}{} if req.Patch.Text != nil { text := strings.TrimSpace(*req.Patch.Text) if text == "" { return nil, app.For(code.Persona).InputMissingRequired("draft text cannot be empty") } patch["text"] = text } if req.Patch.Hook != nil { patch["hook"] = strings.TrimSpace(*req.Patch.Hook) } if req.Patch.Angle != nil { patch["angle"] = strings.TrimSpace(*req.Patch.Angle) } if req.Patch.Status != nil { status := strings.TrimSpace(*req.Patch.Status) if status != "" && status != "pending" && status != "ready" { return nil, app.For(code.Persona).InputMissingRequired("status must be pending or ready") } if status != "" { patch["status"] = status } } if len(patch) == 0 { return nil, app.For(code.Persona).InputMissingRequired("no fields to update") } item, err := u.repo.Update(ctx, req.TenantID, req.OwnerUID, req.PersonaID, draftID, patch) if err != nil { return nil, err } summary := toSummary(*item) return &summary, nil } func (u *copyDraftUseCase) MarkPublished(ctx context.Context, req domusecase.MarkPublishedRequest) (*domusecase.CopyDraftSummary, error) { if err := requireActor(req.TenantID, req.OwnerUID, req.PersonaID); err != nil { return nil, err } draftID := strings.TrimSpace(req.DraftID) mediaID := strings.TrimSpace(req.MediaID) if draftID == "" || mediaID == "" { return nil, app.For(code.Persona).InputMissingRequired("draft_id and media_id are required") } now := clock.NowUnixNano() patch := map[string]interface{}{ "status": "published", "published_media_id": mediaID, "published_permalink": strings.TrimSpace(req.Permalink), "published_at": now, } item, err := u.repo.Update(ctx, req.TenantID, req.OwnerUID, req.PersonaID, draftID, patch) if err != nil { return nil, err } summary := toSummary(*item) return &summary, nil } func (u *copyDraftUseCase) ReplaceMissionMatrix( ctx context.Context, tenantID, ownerUID, personaID, missionID string, drafts []domusecase.CreateRequest, ) ([]domusecase.CopyDraftSummary, error) { if err := requireActor(tenantID, ownerUID, personaID); err != nil { return nil, err } missionID = strings.TrimSpace(missionID) if missionID == "" { return nil, app.For(code.Persona).InputMissingRequired("copy_mission_id is required") } if err := u.repo.DeleteByMissionAndType(ctx, tenantID, ownerUID, personaID, missionID, entity.DraftTypeMatrix); err != nil { return nil, err } return u.CreateMany(ctx, domusecase.CreateManyRequest{ TenantID: tenantID, OwnerUID: ownerUID, PersonaID: personaID, Drafts: drafts, }) } func (u *copyDraftUseCase) ClearByMission(ctx context.Context, tenantID, ownerUID, personaID, missionID string) error { if err := requireActor(tenantID, ownerUID, personaID); err != nil { return err } missionID = strings.TrimSpace(missionID) if missionID == "" { return app.For(code.Persona).InputMissingRequired("copy_mission_id is required") } return u.repo.DeleteByMission(ctx, tenantID, ownerUID, personaID, missionID) } func (u *copyDraftUseCase) ListByMission(ctx context.Context, tenantID, ownerUID, personaID, missionID string, limit int) ([]domusecase.CopyDraftSummary, error) { if err := requireActor(tenantID, ownerUID, personaID); err != nil { return nil, err } items, err := u.repo.ListByMission(ctx, tenantID, ownerUID, personaID, missionID, limit) if err != nil { return nil, err } out := make([]domusecase.CopyDraftSummary, 0, len(items)) for _, item := range items { out = append(out, toSummary(item)) } return out, nil } func (u *copyDraftUseCase) List(ctx context.Context, tenantID, ownerUID, personaID string, limit int) ([]domusecase.CopyDraftSummary, error) { if err := requireActor(tenantID, ownerUID, personaID); err != nil { return nil, err } items, err := u.repo.List(ctx, tenantID, ownerUID, personaID, limit) if err != nil { return nil, err } out := make([]domusecase.CopyDraftSummary, 0, len(items)) for _, item := range items { out = append(out, toSummary(item)) } return out, nil } func requireActor(tenantID, ownerUID, personaID string) error { if strings.TrimSpace(tenantID) == "" || strings.TrimSpace(ownerUID) == "" { return app.For(code.Auth).AuthUnauthorized("missing actor") } if strings.TrimSpace(personaID) == "" { return app.For(code.Persona).InputMissingRequired("persona_id is required") } return nil } func toSummary(item entity.CopyDraft) domusecase.CopyDraftSummary { return domusecase.CopyDraftSummary{ ID: item.ID, PersonaID: item.PersonaID, CopyMissionID: item.CopyMissionID, ScanPostID: item.ScanPostID, DraftType: item.DraftType, SortOrder: item.SortOrder, Text: item.Text, Angle: item.Angle, Hook: item.Hook, Rationale: item.Rationale, ReferenceNotes: item.ReferenceNotes, Sources: item.Sources, Status: item.Status, PublishedMediaID: item.PublishedMediaID, PublishedPermalink: item.PublishedPermalink, PublishedAt: item.PublishedAt, CreateAt: item.CreateAt, } }