Files
warmbly/internal/seed/dev_forms.go
T

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
}