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

130 lines
4.0 KiB
Go
Raw Permalink Normal View History

2026-08-03 05:52:02 +00:00
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
)
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"`
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 {
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
}