mirror of
https://github.com/warmbly/warmbly.git
synced 2026-09-11 00:05:01 +00:00
171 lines
7.7 KiB
Go
171 lines
7.7 KiB
Go
package models
|
|
|
|
import "time"
|
|
|
|
// MaxContactImportRows caps a single import. Large lists should be
|
|
// chunked client-side or routed through a future async-jobs pipeline —
|
|
// blocking a request goroutine on a 500k-row upload is a 504 waiting to
|
|
// happen.
|
|
const MaxContactImportRows = 50000
|
|
|
|
// MaxContactImportPreviewRows is the row count returned by the preview
|
|
// endpoint so the UI can show real data to inform column mapping. Kept
|
|
// small to avoid leaking the entire file back when the user just wants
|
|
// to see "what does this look like".
|
|
const MaxContactImportPreviewRows = 20
|
|
|
|
// ContactImportDedupStrategy decides what happens when a row's email
|
|
// matches an existing contact. "skip" is conservative (HubSpot default),
|
|
// "update" merges new values onto the existing row (Mailchimp's "update
|
|
// existing"), "create_duplicate" forces a new row anyway — useful when
|
|
// the same email is intentionally tracked twice but rare in practice.
|
|
type ContactImportDedupStrategy string
|
|
|
|
const (
|
|
ContactImportDedupSkip ContactImportDedupStrategy = "skip"
|
|
ContactImportDedupUpdate ContactImportDedupStrategy = "update"
|
|
ContactImportDedupCreateDuplicate ContactImportDedupStrategy = "create_duplicate"
|
|
)
|
|
|
|
// ContactImportColumnTarget enumerates where a CSV/XLSX column can be
|
|
// mapped. "ignore" is the no-op that lets users dump a 30-column CRM
|
|
// export and only keep what matters. "custom:<key>" routes the column
|
|
// into Contact.CustomFields.
|
|
type ContactImportColumnTarget string
|
|
|
|
const (
|
|
ContactImportTargetIgnore ContactImportColumnTarget = "ignore"
|
|
ContactImportTargetEmail ContactImportColumnTarget = "email"
|
|
ContactImportTargetFirstName ContactImportColumnTarget = "first_name"
|
|
ContactImportTargetLastName ContactImportColumnTarget = "last_name"
|
|
ContactImportTargetCompany ContactImportColumnTarget = "company"
|
|
ContactImportTargetPhone ContactImportColumnTarget = "phone"
|
|
ContactImportTargetSubscribed ContactImportColumnTarget = "subscribed"
|
|
ContactImportTargetCategories ContactImportColumnTarget = "categories"
|
|
// ContactImportTargetVerificationStatus reads a verdict column written by
|
|
// Warmbly or another verification service (ZeroBounce, MillionVerifier,
|
|
// NeverBounce, ...). Values are recognised by vocabulary; a value nobody
|
|
// knows leaves the contact unverified rather than failing the row.
|
|
ContactImportTargetVerificationStatus ContactImportColumnTarget = "verification_status"
|
|
// ContactImportTargetCustom routes the column into Contact.CustomFields
|
|
// under ContactImportColumnMapping.CustomKey. "custom:<key>" is accepted
|
|
// as an equivalent legacy spelling.
|
|
ContactImportTargetCustom ContactImportColumnTarget = "custom"
|
|
)
|
|
|
|
// ContactImportColumnMapping says "the column at this index maps to
|
|
// this target". For custom fields use a "custom:<key>" target. The key
|
|
// becomes the JSONB column key. The index is zero-based and matches
|
|
// what the preview endpoint returned in `columns`.
|
|
type ContactImportColumnMapping struct {
|
|
Index int `json:"index"`
|
|
Target ContactImportColumnTarget `json:"target"`
|
|
|
|
// VerificationProvider names the vocabulary of a verification_status
|
|
// column when the header or its values made it clear (e.g. "zerobounce").
|
|
// Optional; without it each value is recognised by itself.
|
|
VerificationProvider string `json:"verification_provider,omitempty"`
|
|
|
|
// CustomKey is only used when Target == "custom:<anything>". It
|
|
// is split out so the client can render a nicer label without
|
|
// having to parse the target string.
|
|
CustomKey string `json:"custom_key,omitempty"`
|
|
}
|
|
|
|
type ContactImportPreview struct {
|
|
// Echo what we detected about the file. Filename + Format help the
|
|
// UI render a confirmation; total rows is for "1,243 rows detected"
|
|
// banners.
|
|
Filename string `json:"filename"`
|
|
Format string `json:"format"`
|
|
TotalRows int `json:"total_rows"`
|
|
|
|
// Columns are the headers we found. If the file has no header row,
|
|
// we synthesise "Column 1", "Column 2", ... so the user can still
|
|
// map them. HasHeader records what we decided so the UI can offer
|
|
// a toggle.
|
|
Columns []string `json:"columns"`
|
|
HasHeader bool `json:"has_header"`
|
|
|
|
// Sample rows verbatim. Length is min(N, MaxContactImportPreviewRows).
|
|
SampleRows [][]string `json:"sample_rows"`
|
|
|
|
// Suggested mapping based on header heuristics. The client should
|
|
// treat this as a default the user can override, not a binding
|
|
// decision.
|
|
SuggestedMapping []ContactImportColumnMapping `json:"suggested_mapping"`
|
|
}
|
|
|
|
// ContactImportCommit is the full configuration for committing an
|
|
// import: how to map columns, how to treat collisions, what categories
|
|
// to assign, what the default subscription state is, and which
|
|
// campaign(s) and segment(s) the imported contacts should join.
|
|
type ContactImportCommit struct {
|
|
Mapping []ContactImportColumnMapping `json:"mapping"`
|
|
Dedup ContactImportDedupStrategy `json:"dedup"`
|
|
HasHeader bool `json:"has_header"`
|
|
CategoryIDs []string `json:"category_ids,omitempty"`
|
|
CampaignIDs []string `json:"campaign_ids,omitempty"`
|
|
// SegmentIDs pins every imported row into these segments as a manual
|
|
// include override, the same write the "Add to segment" bulk action does.
|
|
SegmentIDs []string `json:"segment_ids,omitempty"`
|
|
|
|
// SubscribedDefault is what new contacts inherit when no
|
|
// subscribed column was mapped. Defaults to true server-side.
|
|
SubscribedDefault *bool `json:"subscribed_default,omitempty"`
|
|
|
|
// Source / SourceDetail stamp new contacts' first-touch attribution. Set by
|
|
// the caller (file import, Google Sheets sync), never from the request.
|
|
Source ContactSource `json:"-"`
|
|
SourceDetail string `json:"-"`
|
|
}
|
|
|
|
// ContactImportRowError is a row that couldn't be imported. The line
|
|
// number is the 1-based index into the source file (after the header
|
|
// if HasHeader was true) so the user can find it in Excel.
|
|
type ContactImportRowError struct {
|
|
Line int `json:"line"`
|
|
Email string `json:"email,omitempty"`
|
|
Values []string `json:"values,omitempty"`
|
|
Reason string `json:"reason"`
|
|
}
|
|
|
|
// MaxContactImportReportedErrors caps how many per-row entries travel back in
|
|
// the response. The counters still count every row; without the cap a 50k-row
|
|
// file of bad addresses would echo the whole file back as JSON.
|
|
const MaxContactImportReportedErrors = 1000
|
|
|
|
type ContactImportResult struct {
|
|
Total int `json:"total"`
|
|
Imported int `json:"imported"`
|
|
Updated int `json:"updated"`
|
|
Skipped int `json:"skipped"`
|
|
Failed int `json:"failed"`
|
|
StartedAt time.Time `json:"started_at"`
|
|
EndedAt time.Time `json:"ended_at"`
|
|
|
|
// Errors holds per-row failures and per-row notes, capped at
|
|
// MaxContactImportReportedErrors entries.
|
|
Errors []ContactImportRowError `json:"errors,omitempty"`
|
|
// ErrorsTruncated is true when that cap was reached, so the UI can say
|
|
// "showing the first N of M" instead of implying it listed everything.
|
|
ErrorsTruncated bool `json:"errors_truncated,omitempty"`
|
|
|
|
// Quality is what the uploaded addresses look like, measured at import.
|
|
// Advisory: a bad list is reported here and stopped at launch, never
|
|
// refused here, because these are the customer's own records.
|
|
Quality *ContactImportQuality `json:"quality,omitempty"`
|
|
}
|
|
|
|
// ContactImportQuality is an import's address-level assessment.
|
|
type ContactImportQuality struct {
|
|
Malformed int `json:"malformed"`
|
|
Disposable int `json:"disposable"`
|
|
// Role counts shared inboxes. Reported, not counted as bad: mailing info@
|
|
// is a choice, and many legitimate B2B lists are mostly role addresses.
|
|
Role int `json:"role"`
|
|
BadSharePct float64 `json:"bad_share_pct"`
|
|
Flagged bool `json:"flagged"`
|
|
Summary string `json:"summary,omitempty"`
|
|
}
|