12 KiB
Legal page generator
Status: Complete Tasks: #83–85 Tier: 4 (Growth & content)
Goal
Replace the static PreviewData placeholder content on the four policy/legal pages with generated content that's factually accurate for each shop — because Berrypod knows exactly what it does, who processes what data, and how fulfilment works.
Not a generic "fill in the blanks" template. Not LLM-generated waffle. A set of conditional paragraph functions that produce correct, shop-specific content from actual settings and provider data.
The problem with current approach
All four content pages (/privacy, /terms, /delivery, /about) call PreviewData.*_content() which returns hardcoded placeholder text. Shop owners are expected to replace it manually — but most won't, or will copy-paste something generic that doesn't match how Berrypod actually works.
Berrypod already knows:
- Which providers are connected (Printify, Printful — each with different lead times)
- Which countries it ships to (from the
shipping_ratestable) - Whether VAT is enabled and the shop country
- Whether abandoned cart recovery is enabled
- Whether a newsletter is enabled
- The shop name and contact email
This is enough to produce accurate, legally grounded content automatically.
Pages covered
1. Privacy policy (/privacy)
Always included:
- What's collected: name, email, shipping address from orders (legal basis: contract performance, Article 6(1)(b) UK/EU GDPR)
- Payment: processed by Stripe, card data never touches the shop
- Analytics: privacy-first, no cookies, no personal data stored — server-side only, includes device type, country (derived from IP, not stored), referrer
- Cookies: session cookie for cart and auth (strictly necessary, no consent required); country preference cookie for shipping rates. No tracking cookies, no third-party analytics cookies.
- Sharing: shipping details shared with the connected provider(s) — names dynamically inserted
- Retention: order data kept for 7 years (UK statutory accounting requirement); analytics data kept for 2 years
- Contact: shop contact email from settings
- Rights: right of access, rectification, deletion (with caveat: statutory retention periods apply), right to object to marketing
Conditional sections:
-
Abandoned cart recovery enabled (check
Settings.abandoned_cart_recovery_enabled?()— already implemented):Section heading: "Cart recovery"
For UK shops (PECR soft opt-in, Regulation 22):
"If you enter your email address during checkout but don't complete your order, we may send you a single follow-up email about the items in your basket. We do this under the soft opt-in rule in PECR, which permits one reminder email when your address was collected during a checkout you started. We will never send more than one email per abandoned basket. You can unsubscribe at any time using the link included in that email. We store your email address and basket contents for this purpose only, and delete them after 30 days whether or not an email was sent."
For EU shops (GDPR legitimate interests, Article 6(1)(f)):
"If you enter your email address during checkout but don't complete your order, we may send you a single follow-up email about the items in your basket. Our lawful basis is legitimate interests — recovering a genuine sale that was already in progress. We only ever send this once. You can unsubscribe at any time using the link in the email. We store your email address and basket contents for this purpose only, and delete them within 30 days."
For other shops (generic):
"If you start a checkout and don't complete it, we may send you one follow-up email about the items you were buying. We send this once only. You can unsubscribe using the link in the email. We delete this data within 30 days."
In all cases append: "No tracking pixels or click-tracking links are used in these emails."
Implementation notes for the generator:
Settings.abandoned_cart_recovery_enabled?()is already live — just call it- Unsubscribes land at
/unsubscribe/:token(signedPhoenix.Token, 2-year max age), stored inemail_suppressions - Records are in
abandoned_carts, pruned nightly after 30 days byAbandonedCartPruneWorker - The notice shown to customers at Stripe checkout time is the
custom_textfooter added byCheckoutControllerwhen recovery is enabled — cite this as the collection-time notice
-
Newsletter enabled → email marketing section: subscription basis, how to unsubscribe, no third-party sharing
-
Stripe Tax enabled → "Tax calculation is handled by Stripe, which processes transaction and location data to determine applicable rates."
Shop country drives jurisdiction language:
- UK → "under UK GDPR and PECR"
- EU country → "under the EU General Data Protection Regulation (GDPR)"
- US, AU, other → generic "applicable data protection laws"
2. Delivery & returns (/delivery)
This is the most data-rich page — Berrypod has real numbers from the DB.
Production lead times — driven by connected provider(s):
| Provider | Typical production |
|---|---|
| Printify | 2–7 business days |
| Printful | 2–5 business days |
If both providers are connected, show combined note ("production times vary by product").
Shipping destinations — derived from shipping_rates table:
- Query distinct countries with rates → group into regions → list with approximate delivery windows
- If no shipping data: generic placeholder (fallback)
Returns — POD-specific and legally accurate:
This is where generic templates get it wrong. The correct position for print-on-demand:
- Consumer Contracts Regulations Regulation 28(1)(b) — "goods made to the consumer's specifications or clearly personalised" are exempt from the 14-day statutory right to cancel. Every POD product qualifies. This is the legal exemption that applies, and most shops don't cite it correctly.
- Consumer Rights Act 2015 still applies to defective goods — if the item arrives damaged or with a printing defect, the customer is entitled to a repair, replacement, or refund.
- The generated policy states this clearly: no change-of-mind returns (citing the exemption), but reprints/refunds for defects (citing CRA).
Contact and cancellation window:
- Contact email from settings
- Cancellation window: ~2 hours after ordering (before production begins)
3. Terms of service (/terms)
Always included:
- Governing law: driven by shop country setting
- UK → "English law"
- Ireland → "Irish law and EU regulations"
- etc.
- Products: made to order, colour variance disclaimer, all sales final (with returns caveat)
- Payment: via Stripe, orders only confirmed on successful payment
- Intellectual property: designs are the property of the shop owner; customers receive a licence for personal use
- Limitations: we're not liable for delays caused by the print provider or postal service
- Changes: terms may be updated, current version always at this URL
Conditional:
- VAT enabled + registered → "prices include VAT where applicable"
- Newsletter → marketing communications clause
4. Cookie policy
Currently a section within the privacy policy. Can be a standalone page if desired, or remain embedded.
Berrypod's actual cookies (exhaustive):
| Cookie | Purpose | Duration | Consent required? |
|---|---|---|---|
_berrypod_session |
Session state: cart contents, auth | Session | No — strictly necessary |
country_code |
Remember shipping country preference | 1 year | No — strictly necessary for service |
That's it. No analytics cookies. No tracking. No third-party embeds. The generated cookie policy is short and accurate.
5. About page
Not generated — it's the shop owner's own story, Berrypod can't write that for them. But the existing placeholder template should be clearly labelled as placeholder and easy to replace. The page editor (task #19) handles this properly. No changes needed here for Phase 1.
Content block format
The generator produces lists of content_blocks in the format already used by <.rich_text>:
[
%{type: :lead, text: "..."},
%{type: :heading, text: "..."},
%{type: :paragraph, text: "..."},
%{type: :list, items: ["...", "..."]},
%{type: :closing, text: "..."}
]
No changes to the template layer — it already knows how to render these. The generator just produces better data.
Generator module
lib/berrypod/legal_pages.ex — one public function per page:
defmodule Berrypod.LegalPages do
alias Berrypod.{Settings, Shipping, Providers}
def privacy_content do
shop_name = Settings.get_setting("shop_name") || "this shop"
contact_email = Settings.get_setting("contact_email")
shop_country = Settings.get_setting("shop_country", "GB")
# Settings.abandoned_cart_recovery_enabled?/0 is already implemented
abandoned_cart_enabled = Settings.abandoned_cart_recovery_enabled?()
newsletter_enabled = Settings.get_setting("newsletter_enabled", false)
base_sections()
|> maybe_add_abandoned_cart(abandoned_cart_enabled, shop_country)
|> maybe_add_newsletter(newsletter_enabled)
|> add_jurisdiction(shop_country)
|> add_contact(shop_name, contact_email)
end
def delivery_content do
providers = Providers.connected_providers()
shipping_countries = Shipping.list_countries_with_rates()
production_section(providers)
++ shipping_section(shipping_countries)
++ returns_section()
++ cancellation_section()
end
def terms_content do
shop_name = Settings.get(:shop_name) || "this shop"
shop_country = Settings.get(:shop_country, "GB")
vat_enabled = Settings.get(:vat_enabled, false)
base_terms(shop_name)
|> add_governing_law(shop_country)
|> maybe_add_vat_clause(vat_enabled)
end
end
Two phases
Phase 1 — replace PreviewData (no page editor needed)
Wire LegalPages.*_content() into the existing Content LiveView, replacing the three PreviewData.*_content() calls for privacy, delivery, and terms. The about page stays as-is (it's the shop owner's story).
The generated content shows in the live shop immediately. No admin UI needed yet — the content is always accurate because it reflects real settings.
Phase 2 — page editor integration
When the page editor (task #19) ships, add:
- "Regenerate from settings" button per page — reruns the generator and replaces stored content
- Content marked as "auto-generated" vs "customised" — so the admin can tell what's been manually edited
- Generator runs automatically when relevant settings change (provider connected, VAT toggled, abandoned cart enabled) — a PubSub broadcast triggers regeneration
What the generator is and isn't
Is:
- Factually accurate based on real Berrypod behaviour
- Legally grounded (cites correct UK statutes: PECR, Consumer Contracts Regulations, Consumer Rights Act)
- Useful as a starting point that's better than any generic template
Isn't:
- Legal advice. The generated pages include a brief footer note: "This policy was auto-generated based on how this shop is configured. You should review it and seek independent legal advice if you're unsure."
- Comprehensive for edge cases (international VAT registration, non-UK statutory frameworks beyond GDPR)
- A substitute for a solicitor if the shop does complex things
Files to create/modify
lib/berrypod/legal_pages.ex— new, generator functions for each pagelib/berrypod_web/live/shop/content.ex— replace threePreviewData.*_content()calls withLegalPages.*_content()- Phase 2: page editor admin UI for saved/regenerated page content
Tasks
| # | Task | Est |
|---|---|---|
| 83 | LegalPages module — generate accurate privacy, delivery, and terms content from settings + provider + shipping data |
2.5h |
| 84 | Wire LegalPages into Content LiveView — replace PreviewData calls, add tests |
45m |
| 85 | Page editor integration — "Regenerate" button, auto-regenerate on settings change, customised vs auto label | 1.5h (depends on task #19) |