thread-master/apps/backend/internal/module/crm/domain/contact.go

136 lines
4.3 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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通知達此次數仍無動作即 escalatedspec 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
}