package usecase import ( "context" "fmt" "apps/backend/internal/module/radar/domain" ) func (s *Service) GetOpportunity(ctx context.Context, ownerUID int64, id string) (*domain.Opportunity, error) { o, err := s.Repo.GetOpportunity(ctx, id) if err != nil { return nil, err } if o.OwnerUID != ownerUID { return nil, domain.ErrForbidden } return o, nil } func (s *Service) ListOpportunities(ctx context.Context, ownerUID int64, f domain.OpportunityListFilter) ([]*domain.Opportunity, int64, error) { return s.Repo.ListOpportunities(ctx, ownerUID, f) } func (s *Service) ListSweeps(ctx context.Context, ownerUID int64, f domain.SweepListFilter) ([]*domain.RadarSweep, int64, error) { return s.Repo.ListSweeps(ctx, ownerUID, f) } // TriggerSweep enqueues a radar_sweep job for a watch (manual). Same path as daily schedule. func (s *Service) TriggerSweep(ctx context.Context, ownerUID int64, watchID string) (jobID string, err error) { if s.SweepJobs == nil { return "", fmt.Errorf("%w: sweep scheduler not configured", domain.ErrNotReady) } w, err := s.Repo.GetWatch(ctx, watchID) if err != nil { return "", err } if w.OwnerUID != ownerUID { return "", domain.ErrForbidden } if w.Status != domain.WatchActive { return "", fmt.Errorf("%w: only active watches can be swept (status=%s)", domain.ErrValidation, w.Status) } // Manual trigger is due now. return s.SweepJobs.ScheduleRadarSweep(ctx, ownerUID, watchID, domain.NowNano()) } func (s *Service) AcceptOpportunity(ctx context.Context, ownerUID int64, id string) (*domain.Opportunity, string, error) { o, err := s.GetOpportunity(ctx, ownerUID, id) if err != nil { return nil, "", err } if err := o.Transition(domain.OppAccepted); err != nil { return nil, "", err } contactID := o.ContactID if s.CRM != nil { cid, berr := s.CRM.BindOpportunity(ctx, ownerUID, o) if berr != nil { return nil, "", berr } contactID = cid o.ContactID = cid } if err := s.Repo.UpdateOpportunityStatus(ctx, id, domain.OppAccepted); err != nil { return nil, "", err } if contactID != "" { // store contact_id via override path isn't right — re-upsert fields o2, _ := s.Repo.GetOpportunity(ctx, id) if o2 != nil { o2.ContactID = contactID o2.Status = domain.OppAccepted // Use SetOpportunityOverride no-op and a simple save: Upsert won't rewrite existing without terms merge. // Patch via SetOverride with status already updated — add contact via list reload. _ = s.setContactID(ctx, id, contactID) o2.ContactID = contactID return o2, contactID, nil } } o, err = s.Repo.GetOpportunity(ctx, id) return o, contactID, err } func (s *Service) setContactID(ctx context.Context, id, contactID string) error { type patcher interface { SetOpportunityContact(ctx context.Context, id, contactID string) error } if p, ok := s.Repo.(patcher); ok { return p.SetOpportunityContact(ctx, id, contactID) } return nil } func (s *Service) DismissOpportunity(ctx context.Context, ownerUID int64, id, reason string) (*domain.Opportunity, error) { o, err := s.GetOpportunity(ctx, ownerUID, id) if err != nil { return nil, err } if err := o.Transition(domain.OppDismissed); err != nil { return nil, err } if err := s.Repo.UpdateOpportunityStatus(ctx, id, domain.OppDismissed); err != nil { return nil, err } _ = reason return s.Repo.GetOpportunity(ctx, id) } func (s *Service) OverrideOpportunity(ctx context.Context, ownerUID int64, id, band, status string) (*domain.Opportunity, error) { o, err := s.GetOpportunity(ctx, ownerUID, id) if err != nil { return nil, err } ov := &domain.OpportunityOverride{ FromBand: o.IntentBand, ToBand: band, FromStatus: o.Status, ToStatus: status, ActorUID: ownerUID, At: domain.NowNano(), } if status == "" { status = o.Status } if band == "" { band = o.IntentBand } if err := s.Repo.SetOpportunityOverride(ctx, id, ov, band, status); err != nil { return nil, err } return s.Repo.GetOpportunity(ctx, id) }