mirror of
https://github.com/warmbly/warmbly.git
synced 2026-08-19 08:01:16 +00:00
8d351d1f0f
- gofmt-align ContactEngagement fields and drop trailing blank line in contact/export.go - remove no-op self-assignment ac.CustomFields = ac.CustomFields flagged by govet
251 lines
10 KiB
Go
251 lines
10 KiB
Go
package models
|
|
|
|
import (
|
|
"time"
|
|
|
|
"github.com/google/uuid"
|
|
)
|
|
|
|
// MiniCategory is the lightweight shape we attach to contact responses
|
|
// so the UI can render category chips without doing a second lookup. It
|
|
// is a denormalised slice of the row in `categories` plus nothing else.
|
|
type MiniCategory struct {
|
|
ID uuid.UUID `json:"id"`
|
|
Title string `json:"title"`
|
|
Color string `json:"color"`
|
|
}
|
|
|
|
type Contact struct {
|
|
ID uuid.UUID `json:"id"`
|
|
|
|
FirstName string `json:"first_name"`
|
|
LastName string `json:"last_name"`
|
|
Email string `json:"email"`
|
|
Company string `json:"company"`
|
|
Phone string `json:"phone"`
|
|
|
|
CustomFields map[string]string `json:"custom_fields"`
|
|
|
|
Subscribed bool `json:"subscribed"`
|
|
Campaigns []MiniCampaign `json:"campaigns"`
|
|
Categories []MiniCategory `json:"categories"`
|
|
|
|
UpdatedAt time.Time `json:"updated_at"`
|
|
CreatedAt time.Time `json:"created_at"`
|
|
}
|
|
|
|
type ContactsResult struct {
|
|
Data []Contact `json:"data"`
|
|
Pagination Pagination `json:"pagination"`
|
|
}
|
|
|
|
// ContactEngagement summarises every email touchpoint we have for a
|
|
// single contact. It's denormalised on read so the contact 360 view
|
|
// can render counts and "last X" timestamps in a single round-trip.
|
|
type ContactEngagement struct {
|
|
TotalSent int `json:"total_sent"`
|
|
TotalOpened int `json:"total_opened"`
|
|
TotalClicked int `json:"total_clicked"`
|
|
TotalReplied int `json:"total_replied"`
|
|
TotalBounced int `json:"total_bounced"`
|
|
TotalComplained int `json:"total_complained"`
|
|
|
|
LastSentAt *time.Time `json:"last_sent_at,omitempty"`
|
|
LastOpenedAt *time.Time `json:"last_opened_at,omitempty"`
|
|
LastClickedAt *time.Time `json:"last_clicked_at,omitempty"`
|
|
LastRepliedAt *time.Time `json:"last_replied_at,omitempty"`
|
|
LastBouncedAt *time.Time `json:"last_bounced_at,omitempty"`
|
|
}
|
|
|
|
// ContactSuppression mirrors a row from suppressed_recipients for the
|
|
// contact's email. Null on the wire when the contact is not suppressed.
|
|
type ContactSuppression struct {
|
|
Reason string `json:"reason"`
|
|
Source string `json:"source"` // bounce | complaint | unsubscribe
|
|
ExpiresAt *time.Time `json:"expires_at,omitempty"`
|
|
CreatedAt time.Time `json:"created_at"`
|
|
}
|
|
|
|
// ContactDetail is the hydrated read model returned by GET /contacts/:id.
|
|
// It bundles everything the slide-over needs in one payload so the UI
|
|
// doesn't have to fan out a half-dozen requests on open.
|
|
type ContactDetail struct {
|
|
Contact
|
|
Engagement ContactEngagement `json:"engagement"`
|
|
Suppression *ContactSuppression `json:"suppression,omitempty"`
|
|
}
|
|
|
|
// ContactSentEmail is one row in the "Emails sent to this contact"
|
|
// list. Each row corresponds to a single delivered (or attempted)
|
|
// task. Engagement timestamps come from campaign_contact_progress
|
|
// when present; some legacy rows may have nil progress.
|
|
type ContactSentEmail struct {
|
|
TaskID uuid.UUID `json:"task_id"`
|
|
Status string `json:"status"`
|
|
MessageID string `json:"message_id"`
|
|
Subject string `json:"subject"`
|
|
SentAt time.Time `json:"sent_at"`
|
|
|
|
// Sender mailbox
|
|
EmailAccountID *uuid.UUID `json:"email_account_id,omitempty"`
|
|
EmailAccountEmail *string `json:"email_account_email,omitempty"`
|
|
EmailAccountName *string `json:"email_account_name,omitempty"`
|
|
|
|
// Campaign + sequence context
|
|
CampaignID *uuid.UUID `json:"campaign_id,omitempty"`
|
|
CampaignName *string `json:"campaign_name,omitempty"`
|
|
SequenceID *uuid.UUID `json:"sequence_id,omitempty"`
|
|
SequenceName *string `json:"sequence_name,omitempty"`
|
|
|
|
// Engagement (from campaign_contact_progress, may be nil).
|
|
OpenedAt *time.Time `json:"opened_at,omitempty"`
|
|
ClickedAt *time.Time `json:"clicked_at,omitempty"`
|
|
RepliedAt *time.Time `json:"replied_at,omitempty"`
|
|
BouncedAt *time.Time `json:"bounced_at,omitempty"`
|
|
}
|
|
|
|
type ContactSentEmailsResult struct {
|
|
Data []ContactSentEmail `json:"data"`
|
|
Pagination Pagination `json:"pagination"`
|
|
}
|
|
|
|
// ContactTimelineEventType is a closed enum so the frontend can pick
|
|
// the right icon / colour without parsing free text.
|
|
type ContactTimelineEventType string
|
|
|
|
const (
|
|
TimelineEmailSent ContactTimelineEventType = "email_sent"
|
|
TimelineEmailOpened ContactTimelineEventType = "email_opened"
|
|
TimelineEmailClicked ContactTimelineEventType = "email_clicked"
|
|
TimelineEmailReplied ContactTimelineEventType = "email_replied"
|
|
TimelineEmailBounced ContactTimelineEventType = "email_bounced"
|
|
TimelineReplyReceived ContactTimelineEventType = "reply_received"
|
|
TimelineDeliverability ContactTimelineEventType = "deliverability"
|
|
TimelineSuppressed ContactTimelineEventType = "suppressed"
|
|
TimelineNote ContactTimelineEventType = "note"
|
|
)
|
|
|
|
// ContactTimelineEvent is one entry in the merged activity feed. The
|
|
// optional fields are tagged with omitempty so the JSON stays compact
|
|
// for event types that don't carry that data.
|
|
type ContactTimelineEvent struct {
|
|
Type ContactTimelineEventType `json:"type"`
|
|
At time.Time `json:"at"`
|
|
|
|
// Mailbox sender (email_sent / opened / clicked / replied / bounced).
|
|
EmailAccountID *uuid.UUID `json:"email_account_id,omitempty"`
|
|
EmailAccountEmail *string `json:"email_account_email,omitempty"`
|
|
EmailAccountName *string `json:"email_account_name,omitempty"`
|
|
|
|
// Campaign / sequence linkage. Optional because notes, suppression,
|
|
// and out-of-campaign reply intents don't always have one.
|
|
CampaignID *uuid.UUID `json:"campaign_id,omitempty"`
|
|
CampaignName *string `json:"campaign_name,omitempty"`
|
|
SequenceID *uuid.UUID `json:"sequence_id,omitempty"`
|
|
SequenceName *string `json:"sequence_name,omitempty"`
|
|
|
|
// Task linkage for engagement events.
|
|
TaskID *uuid.UUID `json:"task_id,omitempty"`
|
|
Subject *string `json:"subject,omitempty"`
|
|
|
|
// Type-specific.
|
|
Reason *string `json:"reason,omitempty"` // deliverability / suppression
|
|
Source *string `json:"source,omitempty"` // suppression: bounce/complaint/unsubscribe
|
|
Provider *string `json:"provider,omitempty"` // deliverability provider
|
|
Intent *string `json:"intent,omitempty"` // reply_intent classification
|
|
Content *string `json:"content,omitempty"` // note body
|
|
|
|
// Author (notes).
|
|
UserID *uuid.UUID `json:"user_id,omitempty"`
|
|
}
|
|
|
|
type ContactTimelineResult struct {
|
|
Data []ContactTimelineEvent `json:"data"`
|
|
// True if we hit the per-call cap and the caller should paginate
|
|
// via the `before` query param.
|
|
HasMore bool `json:"has_more"`
|
|
}
|
|
|
|
type UpdateContact struct {
|
|
FirstName *string `json:"first_name"`
|
|
LastName *string `json:"last_name"`
|
|
Company *string `json:"company"`
|
|
Phone *string `json:"phone"`
|
|
CustomFields *map[string]string `json:"custom_fields"`
|
|
Subscribed *bool `json:"subscribed"`
|
|
Campaigns []string `json:"campaigns"` // List of campaign IDs to set (nil = leave as-is)
|
|
Categories []string `json:"categories"` // List of category IDs to set (nil = leave as-is)
|
|
AddCategories []string `json:"add_categories"` // Diff-style add (ignored when Categories is set)
|
|
RemoveCategories []string `json:"remove_categories"` // Diff-style remove (ignored when Categories is set)
|
|
}
|
|
|
|
type AddContact struct {
|
|
FirstName string `json:"first_name"`
|
|
LastName string `json:"last_name"`
|
|
Email string `json:"email"`
|
|
Company string `json:"company"`
|
|
Phone string `json:"phone"`
|
|
Campaigns []string `json:"campaigns"`
|
|
Categories []string `json:"categories"`
|
|
|
|
CustomFields map[string]string `json:"custom_fields"`
|
|
}
|
|
|
|
type SearchContactsFilterType string
|
|
|
|
const (
|
|
SearchContactsFilterTypeEqual SearchContactsFilterType = "equal"
|
|
SearchContactsFilterTypeStartsWith SearchContactsFilterType = "starts_with"
|
|
SearchContactsFilterTypeEndsWith SearchContactsFilterType = "ends_with"
|
|
SearchContactsFilterTypeContains SearchContactsFilterType = "contains"
|
|
)
|
|
|
|
type SearchContactsFilter struct {
|
|
Name string `json:"name"`
|
|
Value string `json:"value"`
|
|
Type SearchContactsFilterType `json:"type"`
|
|
}
|
|
|
|
type SearchContacts struct {
|
|
Query string `json:"query"` // Text search across core fields
|
|
CustomFieldFilters []SearchContactsFilter `json:"custom_field_filters"` // Custom Field Filters
|
|
CampaignIDs []string `json:"campaign_ids"` // Contacts must be in ALL these campaigns
|
|
CategoryIDs []string `json:"category_ids"` // Contacts must have ALL these categories
|
|
MinCampaigns *int `json:"min_campaigns"` // Minimum number of associated campaigns
|
|
MaxCampaigns *int `json:"max_campaigns"` // Maximum number of associated campaigns
|
|
Subscribed *bool `json:"subscribed"` // Filter by subscription status
|
|
CreatedAfter *time.Time `json:"created_after"` // Contacts created after this date
|
|
CreatedBefore *time.Time `json:"created_before"` // Contacts created before this date
|
|
UpdatedAfter *time.Time `json:"updated_after"` // Contacts updated after this date
|
|
UpdatedBefore *time.Time `json:"updated_before"` // Contacts updated before this date
|
|
SortBy string `json:"sort_by"` // e.g., "first_name ASC", "campaign_count DESC"
|
|
Reverse bool `json:"reverse"` // ASC or DESC
|
|
Offset int `json:"offset"` // Pagination
|
|
}
|
|
|
|
type BulkEditContactsFieldType string
|
|
|
|
const (
|
|
BulkAddField BulkEditContactsFieldType = "ADD"
|
|
BulkEditField BulkEditContactsFieldType = "EDIT"
|
|
BulkDeleteField BulkEditContactsFieldType = "DELETE"
|
|
BulkRenameField BulkEditContactsFieldType = "RENAME"
|
|
)
|
|
|
|
type BulkEditContactsField struct {
|
|
Type BulkEditContactsFieldType `json:"type"`
|
|
Key string `json:"key"`
|
|
Value string `json:"value"`
|
|
}
|
|
|
|
type BulkEditContactsData struct {
|
|
Contacts []string `json:"contacts"`
|
|
|
|
AddCampaigns []string `json:"add_campaigns"`
|
|
RemoveCampaigns []string `json:"remove_campaigns"`
|
|
AddCategories []string `json:"add_categories,omitempty"`
|
|
RemoveCategories []string `json:"remove_categories,omitempty"`
|
|
Fields []BulkEditContactsField `json:"fields"`
|
|
Subscribe *bool `json:"subscribe"`
|
|
}
|