136 lines
4.3 KiB
Go
136 lines
4.3 KiB
Go
package domain
|
||
|
||
import (
|
||
"fmt"
|
||
"strings"
|
||
"time"
|
||
|
||
"github.com/google/uuid"
|
||
)
|
||
|
||
func NowNano() int64 { return time.Now().UTC().UnixNano() }
|
||
func NewID() string { return uuid.NewString() }
|
||
|
||
const (
|
||
StageNewFound = "new_found"
|
||
StageEngaged = "engaged"
|
||
StageDMSent = "dm_sent"
|
||
StageReplied = "replied"
|
||
StageQuoted = "quoted"
|
||
StageWon = "won"
|
||
StageLost = "lost"
|
||
|
||
PlatformThreads = "threads"
|
||
|
||
TouchStage = "stage"
|
||
TouchReply = "reply"
|
||
TouchNote = "note"
|
||
TouchConversion = "conversion"
|
||
|
||
FollowUpScheduled = "scheduled"
|
||
FollowUpNotified = "notified"
|
||
FollowUpDone = "done"
|
||
FollowUpSnoozed = "snoozed"
|
||
FollowUpEscalated = "escalated"
|
||
|
||
DefaultFollowUpDays = 3
|
||
// MaxFollowUpNotifications:通知達此次數仍無動作即 escalated(spec FU-03)。
|
||
MaxFollowUpNotifications = 2
|
||
)
|
||
|
||
type Contact struct {
|
||
ID string `bson:"_id" json:"id"`
|
||
OwnerUID int64 `bson:"owner_uid" json:"owner_uid"`
|
||
SourcePlatform string `bson:"source_platform" json:"source_platform"`
|
||
AuthorHandle string `bson:"author_handle" json:"author_handle"`
|
||
DisplayName string `bson:"display_name,omitempty" json:"display_name,omitempty"`
|
||
Stage string `bson:"stage" json:"stage"`
|
||
NeedsFollowUp bool `bson:"needs_follow_up" json:"needs_follow_up"`
|
||
FollowUpDays int `bson:"follow_up_days" json:"follow_up_days"`
|
||
LastTouchAt int64 `bson:"last_touch_at,omitempty" json:"last_touch_at,omitempty"`
|
||
OpportunityIDs []string `bson:"opportunity_ids" json:"opportunity_ids"`
|
||
MergedFrom []string `bson:"merged_from,omitempty" json:"merged_from,omitempty"`
|
||
TopIntentBand string `bson:"top_intent_band,omitempty" json:"top_intent_band,omitempty"`
|
||
TopIntentScore int `bson:"top_intent_score,omitempty" json:"top_intent_score,omitempty"`
|
||
// OutcomeID links the latest conversion in growth_outcomes.
|
||
OutcomeID string `bson:"outcome_id,omitempty" json:"outcome_id,omitempty"`
|
||
// RemovedAt hides the contact from the working list without breaking
|
||
// opportunity, touch, or conversion history that still references its ID.
|
||
RemovedAt int64 `bson:"removed_at,omitempty" json:"removed_at,omitempty"`
|
||
CreatedAt int64 `bson:"created_at" json:"created_at"`
|
||
UpdatedAt int64 `bson:"updated_at" json:"updated_at"`
|
||
}
|
||
|
||
type ContactTouch struct {
|
||
ID string `bson:"_id" json:"id"`
|
||
OwnerUID int64 `bson:"owner_uid" json:"owner_uid"`
|
||
ContactID string `bson:"contact_id" json:"contact_id"`
|
||
Type string `bson:"type" json:"type"`
|
||
FromStage string `bson:"from_stage,omitempty" json:"from_stage,omitempty"`
|
||
ToStage string `bson:"to_stage,omitempty" json:"to_stage,omitempty"`
|
||
Body string `bson:"body,omitempty" json:"body,omitempty"`
|
||
ActorUID int64 `bson:"actor_uid" json:"actor_uid"`
|
||
CreatedAt int64 `bson:"created_at" json:"created_at"`
|
||
}
|
||
|
||
type FollowUp struct {
|
||
ID string `bson:"_id" json:"id"`
|
||
OwnerUID int64 `bson:"owner_uid" json:"owner_uid"`
|
||
ContactID string `bson:"contact_id" json:"contact_id"`
|
||
DueAt int64 `bson:"due_at" json:"due_at"`
|
||
Status string `bson:"status" json:"status"`
|
||
NotifiedCount int `bson:"notified_count" json:"notified_count"`
|
||
CreatedAt int64 `bson:"created_at" json:"created_at"`
|
||
UpdatedAt int64 `bson:"updated_at" json:"updated_at"`
|
||
}
|
||
|
||
type ContactListFilter struct {
|
||
Query string
|
||
Stage string
|
||
FollowUp *bool
|
||
Band string
|
||
Sort string // last_touch_at | intent_score
|
||
Page int
|
||
PageSize int
|
||
}
|
||
|
||
type FollowUpListFilter struct {
|
||
Status string
|
||
Page int
|
||
PageSize int
|
||
}
|
||
|
||
func IsStage(s string) bool {
|
||
switch s {
|
||
case StageNewFound, StageEngaged, StageDMSent, StageReplied, StageQuoted, StageWon, StageLost:
|
||
return true
|
||
}
|
||
return false
|
||
}
|
||
|
||
func (c *Contact) Normalize() error {
|
||
if c.OwnerUID <= 0 {
|
||
return fmt.Errorf("%w: owner_uid required", ErrValidation)
|
||
}
|
||
c.AuthorHandle = strings.TrimPrefix(strings.TrimSpace(c.AuthorHandle), "@")
|
||
if c.AuthorHandle == "" {
|
||
return fmt.Errorf("%w: author_handle required", ErrValidation)
|
||
}
|
||
if c.SourcePlatform == "" {
|
||
c.SourcePlatform = PlatformThreads
|
||
}
|
||
if c.Stage == "" {
|
||
c.Stage = StageNewFound
|
||
}
|
||
if !IsStage(c.Stage) {
|
||
return fmt.Errorf("%w: unknown stage %q", ErrValidation, c.Stage)
|
||
}
|
||
if c.FollowUpDays <= 0 {
|
||
c.FollowUpDays = DefaultFollowUpDays
|
||
}
|
||
if c.OpportunityIDs == nil {
|
||
c.OpportunityIDs = []string{}
|
||
}
|
||
return nil
|
||
}
|