71 lines
2.2 KiB
Go
71 lines
2.2 KiB
Go
|
|
package usecase
|
||
|
|
|
||
|
|
import (
|
||
|
|
"context"
|
||
|
|
"fmt"
|
||
|
|
"strings"
|
||
|
|
|
||
|
|
"apps/backend/internal/module/radar/domain"
|
||
|
|
)
|
||
|
|
|
||
|
|
// HealthGate checks AccountHealth before auto-send (outbox path).
|
||
|
|
// Level: ok | warn | throttle. Throttle must block automatic send.
|
||
|
|
type HealthGate interface {
|
||
|
|
// WorstLevel returns the worst health level among usable accounts for owner.
|
||
|
|
WorstLevel(ctx context.Context, ownerUID int64) (level string, advice string, err error)
|
||
|
|
}
|
||
|
|
|
||
|
|
// MarkReplyUsed records that a draft was sent or copied (T550).
|
||
|
|
// channel: outbox | manual_copy
|
||
|
|
// - dm variants: only manual_copy
|
||
|
|
// - outbox: rejects when health=throttle; warn still allowed (caller may surface advice)
|
||
|
|
func (s *Service) MarkReplyUsed(ctx context.Context, ownerUID int64, opportunityID, replyID, channel string) (*domain.ReplyVariant, string, error) {
|
||
|
|
channel = strings.TrimSpace(channel)
|
||
|
|
if channel != domain.SentOutbox && channel != domain.SentManualCopy {
|
||
|
|
return nil, "", fmt.Errorf("%w: channel must be outbox or manual_copy", domain.ErrValidation)
|
||
|
|
}
|
||
|
|
o, err := s.GetOpportunity(ctx, ownerUID, opportunityID)
|
||
|
|
if err != nil {
|
||
|
|
return nil, "", err
|
||
|
|
}
|
||
|
|
_ = o
|
||
|
|
r, err := s.Repo.GetReply(ctx, replyID)
|
||
|
|
if err != nil {
|
||
|
|
return nil, "", err
|
||
|
|
}
|
||
|
|
if r.OwnerUID != ownerUID || r.OpportunityID != opportunityID {
|
||
|
|
return nil, "", domain.ErrForbidden
|
||
|
|
}
|
||
|
|
if r.Variant == domain.ReplyDM && channel == domain.SentOutbox {
|
||
|
|
return nil, "", fmt.Errorf("%w: dm reply cannot auto-send; use manual_copy", domain.ErrValidation)
|
||
|
|
}
|
||
|
|
|
||
|
|
var healthAdvice string
|
||
|
|
if channel == domain.SentOutbox {
|
||
|
|
if s.Health == nil {
|
||
|
|
// No gate wired: still allow mark-used so offline demos work; production wires Health.
|
||
|
|
} else {
|
||
|
|
level, advice, herr := s.Health.WorstLevel(ctx, ownerUID)
|
||
|
|
if herr != nil {
|
||
|
|
return nil, "", herr
|
||
|
|
}
|
||
|
|
if level == "throttle" {
|
||
|
|
return nil, advice, fmt.Errorf("%w: account health throttle blocks auto-send; copy and send manually", domain.ErrValidation)
|
||
|
|
}
|
||
|
|
if level == "warn" {
|
||
|
|
healthAdvice = advice
|
||
|
|
if healthAdvice == "" {
|
||
|
|
healthAdvice = "帳號健康度偏黃,建議放慢自動送出。"
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
r.UsedAt = domain.NowNano()
|
||
|
|
r.SentChannel = channel
|
||
|
|
if err := s.Repo.SaveReply(ctx, r); err != nil {
|
||
|
|
return nil, "", err
|
||
|
|
}
|
||
|
|
return r, healthAdvice, nil
|
||
|
|
}
|