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 }