134 lines
3.9 KiB
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)
|
||
|
|
}
|