package profile import ( "context" "errors" "strings" "time" "github.com/google/uuid" "github.com/yuxingu/digital-psychology/apps/api/internal/model" "github.com/yuxingu/digital-psychology/apps/api/internal/repository" "github.com/yuxingu/digital-psychology/apps/api/internal/service/bootstrap" "github.com/yuxingu/digital-psychology/apps/api/internal/textsafe" ) // Service manages personal archives. type Service struct { Repo *repository.ProfileRepo Reports *repository.ReportRepo Bootstrap *bootstrap.Service } // CreateInput is validated create payload. type CreateInput struct { Relation string DisplayName string BirthDate time.Time RelationType *string BirthTime *string BirthPlace *string } // ErrSelfExists is returned when creating a second self profile. var ErrSelfExists = errors.New("self profile already exists") // Create stores a profile for the user. func (s *Service) Create(ctx context.Context, userID uuid.UUID, in CreateInput) (*model.Profile, error) { if in.Relation != "self" && in.Relation != "other" { return nil, errors.New("relation must be self or other") } if in.BirthDate.IsZero() { return nil, errors.New("birth_date required") } if in.Relation == "self" { n, err := s.Repo.CountActiveSelf(ctx, userID) if err != nil { return nil, err } if n > 0 { return nil, ErrSelfExists } } name := in.DisplayName if name == "" { if in.Relation == "self" { name = "我" } else { name = "TA" } } name, err := textsafe.Check(textsafe.DisplayName, name) if err != nil { return nil, err } p, err := s.Repo.Create(ctx, userID, in.Relation, name, in.BirthDate, in.RelationType, in.BirthTime, in.BirthPlace) if err != nil { return nil, err } if s.Bootstrap != nil { s.Bootstrap.GenerateForProfile(ctx, userID, p) } return p, nil } // List returns user's profiles. func (s *Service) List(ctx context.Context, userID uuid.UUID) ([]model.Profile, error) { return s.Repo.ListByUser(ctx, userID) } // UpdateInput for PATCH /profiles/:id. type UpdateInput struct { DisplayName string BirthDate time.Time RelationType *string BirthTime *string BirthPlace *string GeoLat *float64 GeoLng *float64 GeoVisible *bool } // Update patches an owned profile. func (s *Service) Update(ctx context.Context, userID, profileID uuid.UUID, in UpdateInput) (*model.Profile, error) { cur, err := s.Repo.GetForUser(ctx, userID, profileID) if err != nil { return nil, errors.New("profile not found") } name := strings.TrimSpace(in.DisplayName) if name == "" { name = cur.DisplayName } else { var err error name, err = textsafe.Check(textsafe.DisplayName, name) if err != nil { return nil, err } } birth := in.BirthDate if birth.IsZero() { birth = cur.BirthDate } rt := in.RelationType if rt == nil { rt = cur.RelationType } bt := in.BirthTime if bt == nil { bt = cur.BirthTime } bp := in.BirthPlace if bp == nil { bp = cur.BirthPlace } birthChanged := !in.BirthDate.IsZero() && !sameDay(in.BirthDate, cur.BirthDate) timeChanged := in.BirthTime != nil && strPtr(in.BirthTime) != strPtr(cur.BirthTime) placeChanged := in.BirthPlace != nil && strPtr(in.BirthPlace) != strPtr(cur.BirthPlace) p, err := s.Repo.UpdateForUser(ctx, userID, profileID, name, birth, rt, bt, bp, in.GeoLat, in.GeoLng, in.GeoVisible) if err != nil { return nil, err } if s.Bootstrap != nil && (birthChanged || timeChanged || placeChanged) { s.Bootstrap.GenerateForProfile(ctx, userID, p) } return p, nil } func sameDay(a, b time.Time) bool { ay, am, ad := a.Date() by, bm, bd := b.Date() return ay == by && am == bm && ad == bd } func strPtr(p *string) string { if p == nil { return "" } return *p } // Delete soft-deletes an owned profile. func (s *Service) Delete(ctx context.Context, userID, profileID uuid.UUID) error { if _, err := s.Repo.GetForUser(ctx, userID, profileID); err != nil { return errors.New("profile not found") } if s.Reports != nil { _ = s.Reports.SoftDeleteForProfile(ctx, userID, profileID) } return s.Repo.SoftDeleteForUser(ctx, userID, profileID) }