mirror of
https://github.com/warmbly/warmbly.git
synced 2026-09-05 16:02:48 +00:00
231 lines
9.5 KiB
Go
231 lines
9.5 KiB
Go
package seed
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
|
|
"github.com/google/uuid"
|
|
"github.com/jackc/pgx/v5/pgxpool"
|
|
)
|
|
|
|
// Dev-org forms ("dddd0f" block).
|
|
var (
|
|
devFormDemo = uuid.MustParse("33333333-0000-0000-0000-000000000001")
|
|
devFormNewsletter = uuid.MustParse("33333333-0000-0000-0000-000000000002")
|
|
devFormContact = uuid.MustParse("33333333-0000-0000-0000-000000000003")
|
|
)
|
|
|
|
// devFormDemoPublicID is referenced from the draft campaign's step, so the
|
|
// campaign Forms panel has something to report before anything is sent.
|
|
const devFormDemoPublicID = "bkademe23456789abcdef"
|
|
|
|
func devFormLinkID(i int) uuid.UUID {
|
|
return uuid.MustParse(fmt.Sprintf("dddd0f00-0000-0000-0000-%012d", i))
|
|
}
|
|
|
|
func devFormEventID(journey, step int) uuid.UUID {
|
|
return uuid.MustParse(fmt.Sprintf("dddd0f01-0000-0000-0000-%06d%06d", journey, step))
|
|
}
|
|
|
|
func devFormSubmissionID(i int) uuid.UUID {
|
|
return uuid.MustParse(fmt.Sprintf("dddd0f02-0000-0000-0000-%012d", i))
|
|
}
|
|
|
|
// seedDevForms builds the hosted-forms fixture: a three-page demo form on the
|
|
// split layout, a one-question newsletter form in focus mode and a draft,
|
|
// plus the personalized links, funnel events and submissions that make the
|
|
// analytics, responses and campaign panels show real shapes.
|
|
func seedDevForms(ctx context.Context, pool *pgxpool.Pool) error {
|
|
demoFields := `[
|
|
{"id":"f_head","type":"heading","label":"Book a demo","required":false},
|
|
{"id":"f_intro","type":"paragraph","label":"","value":"Twenty minutes, no slides. We will look at your current sending setup together.","required":false},
|
|
{"id":"f_first","type":"text","label":"First name","required":true,"map_to":"first_name","width":"half"},
|
|
{"id":"f_last","type":"text","label":"Last name","required":false,"map_to":"last_name","width":"half"},
|
|
{"id":"f_email","type":"email","label":"Work email","required":true,"map_to":"email"},
|
|
{"id":"f_break1","type":"page_break","label":"About your team","required":false},
|
|
{"id":"f_company","type":"text","label":"Company","required":false,"map_to":"company"},
|
|
{"id":"f_size","type":"select","label":"How many people send outbound?","required":true,"options":["Just me","2-10","11-50","50+"]},
|
|
{"id":"f_mailboxes","type":"number","label":"Mailboxes in rotation","required":false},
|
|
{"id":"f_break2","type":"page_break","label":"Anything else","required":false},
|
|
{"id":"f_note","type":"textarea","label":"What would you like to cover?","required":false,"rows":4},
|
|
{"id":"f_terms","type":"checkbox","label":"Terms","placeholder":"I agree to be contacted about this request","required":true}
|
|
]`
|
|
demoDesign := `{"theme":"ocean","layout":"split","mode":"classic","align":"left","show_progress":true,
|
|
"font_family":"sora","page_background":"#0c4a6e","page_background_end":"#082f49","form_background":"#ffffff",
|
|
"text_color":"#0f172a","label_color":"#334155","input_background":"#ffffff","input_border_color":"#e2e8f0",
|
|
"input_text_color":"#0f172a","placeholder_color":"#94a3b8","accent_color":"#0284c7",
|
|
"button_background":"#0284c7","button_text_color":"#ffffff","button_text":"Book my demo",
|
|
"border_radius":12,"shadow":true}`
|
|
|
|
newsletterFields := `[
|
|
{"id":"n_email","type":"email","label":"Email","required":true,"map_to":"email"},
|
|
{"id":"n_topic","type":"radio","label":"What should we send you?","required":true,"options":["Product news","Deliverability tips","Both"]}
|
|
]`
|
|
newsletterDesign := `{"theme":"midnight","layout":"card","mode":"focus","align":"center","show_progress":true,
|
|
"font_family":"inter","page_background":"#0f172a","form_background":"#1e293b","text_color":"#f1f5f9",
|
|
"label_color":"#cbd5e1","input_background":"#0f172a","input_border_color":"#334155","input_text_color":"#f1f5f9",
|
|
"placeholder_color":"#64748b","accent_color":"#38bdf8","button_background":"#38bdf8","button_text_color":"#0f172a",
|
|
"button_text":"Subscribe","border_radius":12,"shadow":false}`
|
|
|
|
contactFields := `[
|
|
{"id":"c_name","type":"text","label":"Name","required":true,"map_to":"first_name"},
|
|
{"id":"c_email","type":"email","label":"Email","required":true,"map_to":"email"},
|
|
{"id":"c_msg","type":"textarea","label":"How can we help?","required":true,"rows":5}
|
|
]`
|
|
|
|
forms := []struct {
|
|
id uuid.UUID
|
|
publicID string
|
|
name string
|
|
status string
|
|
fields string
|
|
design string
|
|
views int64
|
|
ageDays int
|
|
}{
|
|
{devFormDemo, devFormDemoPublicID, "Book a demo", "published", demoFields, demoDesign, 412, 35},
|
|
{devFormNewsletter, "newsjxyzsignup2345678", "Newsletter signup", "published", newsletterFields, newsletterDesign, 168, 52},
|
|
{devFormContact, "cntactus23456789abcde", "Contact us", "draft", contactFields, `{"theme":"minimal","layout":"card","mode":"classic"}`, 0, 3},
|
|
}
|
|
|
|
for _, f := range forms {
|
|
var publishedAt any
|
|
if f.status == "published" {
|
|
publishedAt = fmt.Sprintf("%d days", f.ageDays-1)
|
|
}
|
|
if _, err := pool.Exec(ctx, `
|
|
INSERT INTO forms (
|
|
id, organization_id, created_by, public_id, name, status, fields, design,
|
|
success_message, captcha_enabled, views_count, published_at, created_at, updated_at
|
|
) VALUES (
|
|
$1,$2,$3,$4,$5,$6,$7::jsonb,$8::jsonb,$9,false,$10,
|
|
CASE WHEN $11::text IS NULL THEN NULL ELSE now() - $11::interval END,
|
|
now() - make_interval(days => $12), now()
|
|
)
|
|
ON CONFLICT (id) DO UPDATE SET
|
|
public_id = EXCLUDED.public_id,
|
|
name = EXCLUDED.name,
|
|
status = EXCLUDED.status,
|
|
fields = EXCLUDED.fields,
|
|
design = EXCLUDED.design,
|
|
views_count = EXCLUDED.views_count,
|
|
updated_at = now()
|
|
`, f.id, DevOrgID, DevUserID, f.publicID, f.name, f.status, f.fields, f.design,
|
|
"Thanks! We will be in touch shortly.", f.views, publishedAt, f.ageDays); err != nil {
|
|
return fmt.Errorf("form %s: %w", f.name, err)
|
|
}
|
|
}
|
|
|
|
// Demo-form leads are filed under Leads; newsletter signups under Customers.
|
|
for _, b := range []struct {
|
|
form, category uuid.UUID
|
|
}{
|
|
{devFormDemo, devCategoryLead},
|
|
{devFormNewsletter, devCategoryCustomer},
|
|
} {
|
|
if _, err := pool.Exec(ctx, `
|
|
INSERT INTO form_categories (form_id, category_id) VALUES ($1,$2)
|
|
ON CONFLICT DO NOTHING
|
|
`, b.form, b.category); err != nil {
|
|
return fmt.Errorf("form category: %w", err)
|
|
}
|
|
}
|
|
|
|
return seedDevFormFunnel(ctx, pool)
|
|
}
|
|
|
|
// seedDevFormFunnel walks seven contacts of the active campaign through the
|
|
// demo form: all of them opened their personalized link, most started, fewer
|
|
// reached the later pages and three finished. The ones who stalled are what
|
|
// the responses table shows as "in progress".
|
|
func seedDevFormFunnel(ctx context.Context, pool *pgxpool.Pool) error {
|
|
journeys := []struct {
|
|
contact int
|
|
furthestPage int
|
|
submitted bool
|
|
country string
|
|
device string
|
|
}{
|
|
{0, 2, true, "US", "desktop"},
|
|
{1, 2, true, "GB", "mobile"},
|
|
{2, 2, true, "US", "desktop"},
|
|
{3, 1, false, "DE", "desktop"},
|
|
{4, 1, false, "US", "mobile"},
|
|
{5, 0, false, "CA", "tablet"},
|
|
{6, 0, false, "US", "desktop"},
|
|
}
|
|
|
|
for i, j := range journeys {
|
|
contact := devContactID(j.contact)
|
|
if _, err := pool.Exec(ctx, `
|
|
INSERT INTO form_links (id, form_id, organization_id, contact_id, campaign_id, created_at)
|
|
VALUES ($1,$2,$3,$4,$5, now() - make_interval(days => $6))
|
|
ON CONFLICT (form_id, contact_id) DO NOTHING
|
|
`, devFormLinkID(i+1), devFormDemo, DevOrgID, contact, DevCampaignActiveID, 20-i); err != nil {
|
|
return fmt.Errorf("form link %d: %w", i, err)
|
|
}
|
|
|
|
// A journey is a view, then a start, then one page event per page
|
|
// reached, exactly as the hosted app reports them.
|
|
type ev struct {
|
|
kind string
|
|
page int
|
|
}
|
|
events := []ev{{"view", 0}}
|
|
if j.furthestPage > 0 || j.submitted {
|
|
events = append(events, ev{"start", 0})
|
|
}
|
|
for p := 1; p <= j.furthestPage; p++ {
|
|
events = append(events, ev{"page", p})
|
|
}
|
|
if j.submitted {
|
|
events = append(events, ev{"submit", j.furthestPage})
|
|
}
|
|
|
|
for k, e := range events {
|
|
if _, err := pool.Exec(ctx, `
|
|
INSERT INTO form_events (
|
|
id, organization_id, form_id, event_type, occurred_at, visitor_key,
|
|
page_index, pages_total, contact_id, campaign_id, referrer_domain, country_code, device
|
|
) VALUES (
|
|
$1,$2,$3,$4, now() - make_interval(days => $5, mins => $6), $7,$8,3,$9,$10,
|
|
'mail.google.com',$11,$12
|
|
)
|
|
ON CONFLICT (id) DO NOTHING
|
|
`, devFormEventID(i+1, k+1), DevOrgID, devFormDemo, e.kind, 19-i, 90-k*9,
|
|
fmt.Sprintf("dev-vk-%d", i+1), e.page, contact, DevCampaignActiveID,
|
|
j.country, j.device); err != nil {
|
|
return fmt.Errorf("form event %d/%d: %w", i, k, err)
|
|
}
|
|
}
|
|
|
|
if !j.submitted {
|
|
continue
|
|
}
|
|
c := devContacts[j.contact]
|
|
data := fmt.Sprintf(
|
|
`{"f_first":%q,"f_last":%q,"f_email":%q,"f_company":%q,"f_size":"11-50","f_note":"Mostly worried about landing in spam as we scale.","f_terms":"true"}`,
|
|
c.first, c.last, devContactEmail(c), c.company)
|
|
if _, err := pool.Exec(ctx, `
|
|
INSERT INTO form_submissions (
|
|
id, form_id, organization_id, contact_id, campaign_id, data, source_url, created_at
|
|
) VALUES ($1,$2,$3,$4,$5,$6::jsonb,$7, now() - make_interval(days => $8, hours => 2))
|
|
ON CONFLICT (id) DO NOTHING
|
|
`, devFormSubmissionID(i+1), devFormDemo, DevOrgID, contact, DevCampaignActiveID,
|
|
data, "https://forms.warmbly.com/f/"+devFormDemoPublicID, 18-i); err != nil {
|
|
return fmt.Errorf("form submission %d: %w", i, err)
|
|
}
|
|
}
|
|
|
|
// Keep the denormalized counters in step with the rows written above.
|
|
if _, err := pool.Exec(ctx, `
|
|
UPDATE forms f SET
|
|
submissions_count = (SELECT COUNT(*) FROM form_submissions s WHERE s.form_id = f.id),
|
|
last_submission_at = (SELECT MAX(created_at) FROM form_submissions s WHERE s.form_id = f.id)
|
|
WHERE f.organization_id = $1
|
|
`, DevOrgID); err != nil {
|
|
return fmt.Errorf("form counters: %w", err)
|
|
}
|
|
return nil
|
|
}
|