thread-master/apps/backend/internal/module/radar/usecase/opportunity_ops.go

134 lines
3.9 KiB
Go

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)
}