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/crm_contact/domain/entity" domrepo "haixun-backend/internal/model/crm_contact/domain/repository" domusecase "haixun-backend/internal/model/crm_contact/domain/usecase" "github.com/google/uuid" ) type crmContactUseCase struct { repo domrepo.Repository } func NewUseCase(repo domrepo.Repository) domusecase.UseCase { return &crmContactUseCase{repo: repo} } func (u *crmContactUseCase) Create(ctx context.Context, tenantID, ownerUID, brandID, authorID, authorName string) (*domusecase.CRMContactSummary, error) { if err := requireActor(tenantID, ownerUID); err != nil { return nil, err } authorID = strings.TrimSpace(authorID) if authorID == "" { return nil, app.For(code.Persona).InputMissingRequired("author_id is required") } authorName = strings.TrimSpace(authorName) if authorName == "" { return nil, app.For(code.Persona).InputMissingRequired("author_name is required") } now := clock.NowUnixNano() contact := &entity.CRMContact{ ID: uuid.NewString(), TenantID: tenantID, OwnerUID: ownerUID, BrandID: strings.TrimSpace(brandID), AuthorID: authorID, AuthorName: authorName, Status: entity.ContactStatusLead, CreateAt: now, UpdateAt: now, } if err := u.repo.Create(ctx, contact); err != nil { return nil, err } summary := toSummary(*contact) return &summary, nil } func (u *crmContactUseCase) Get(ctx context.Context, tenantID, ownerUID, contactID string) (*domusecase.CRMContactSummary, error) { if err := requireActor(tenantID, ownerUID); err != nil { return nil, err } contactID = strings.TrimSpace(contactID) if contactID == "" { return nil, app.For(code.Persona).InputMissingRequired("contact_id is required") } item, err := u.repo.Get(ctx, tenantID, ownerUID, contactID) if err != nil { return nil, err } summary := toSummary(*item) return &summary, nil } func (u *crmContactUseCase) Update(ctx context.Context, req domusecase.UpdateRequest) (*domusecase.CRMContactSummary, error) { if err := requireActor(req.TenantID, req.OwnerUID); err != nil { return nil, err } contactID := strings.TrimSpace(req.ContactID) if contactID == "" { return nil, app.For(code.Persona).InputMissingRequired("contact_id is required") } patch := map[string]interface{}{} if req.Notes != nil { patch["notes"] = strings.TrimSpace(*req.Notes) } if req.Tags != nil { patch["tags"] = *req.Tags } if req.Status != nil { status := strings.TrimSpace(*req.Status) if status != "" && status != entity.ContactStatusLead && status != entity.ContactStatusContacted && status != entity.ContactStatusResponded && status != entity.ContactStatusConverted && status != entity.ContactStatusArchived { return nil, app.For(code.Persona).InputInvalidFormat("invalid contact status") } if status != "" { patch["status"] = status } } if len(patch) == 0 { return nil, app.For(code.Persona).InputMissingRequired("no fields to update") } patch["update_at"] = clock.NowUnixNano() item, err := u.repo.Update(ctx, req.TenantID, req.OwnerUID, contactID, patch) if err != nil { return nil, err } summary := toSummary(*item) return &summary, nil } func (u *crmContactUseCase) Delete(ctx context.Context, tenantID, ownerUID, contactID string) error { if err := requireActor(tenantID, ownerUID); err != nil { return err } contactID = strings.TrimSpace(contactID) if contactID == "" { return app.For(code.Persona).InputMissingRequired("contact_id is required") } return u.repo.Delete(ctx, tenantID, ownerUID, contactID) } func (u *crmContactUseCase) List(ctx context.Context, req domusecase.ListRequest) ([]domusecase.CRMContactSummary, int, error) { if err := requireActor(req.TenantID, req.OwnerUID); err != nil { return nil, 0, err } page := req.Page pageSize := req.PageSize if page < 1 { page = 1 } if pageSize < 1 { pageSize = 20 } if pageSize > 100 { pageSize = 100 } filter := domrepo.ListFilter{ BrandID: strings.TrimSpace(req.BrandID), Status: strings.TrimSpace(req.Status), Tag: strings.TrimSpace(req.Tag), Limit: pageSize, Offset: (page - 1) * pageSize, } items, total, err := u.repo.List(ctx, req.TenantID, req.OwnerUID, filter) if err != nil { return nil, 0, err } out := make([]domusecase.CRMContactSummary, 0, len(items)) for _, item := range items { out = append(out, toSummary(item)) } return out, total, nil } func requireActor(tenantID, ownerUID string) error { if strings.TrimSpace(tenantID) == "" || strings.TrimSpace(ownerUID) == "" { return app.For(code.Auth).AuthUnauthorized("missing actor") } return nil } func toSummary(item entity.CRMContact) domusecase.CRMContactSummary { return domusecase.CRMContactSummary{ ID: item.ID, BrandID: item.BrandID, AuthorID: item.AuthorID, AuthorName: item.AuthorName, AuthorAvatar: item.AuthorAvatar, AuthorFollowers: item.AuthorFollowers, ScanPostID: item.ScanPostID, Notes: item.Notes, Tags: item.Tags, Status: item.Status, OutreachCount: item.OutreachCount, LastContactedAt: item.LastContactedAt, CreateAt: item.CreateAt, UpdateAt: item.UpdateAt, } }