berrypod/docs/plans/legal-page-generator.md
jamey 61887b9d5b
All checks were successful
deploy / deploy (push) Successful in 3m32s
improve cart recovery: product links in email, persistent session cookie
- add product_id to order_items (migration + schema + create_order)
- cart recovery email now includes a direct product link per item
- extend session cookie max_age to 7 days so carts survive browser restarts

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-24 13:12:41 +00:00

12 KiB
Raw Blame History

Legal page generator

Status: Planned Tasks: #8385 in PROGRESS.md Tier: 4 (Growth & content), Phase 1 can ship earlier

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_rates table)
  • 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 (signed Phoenix.Token, 2-year max age), stored in email_suppressions
    • Records are in abandoned_carts, pruned nightly after 30 days by AbandonedCartPruneWorker
    • The notice shown to customers at Stripe checkout time is the custom_text footer added by CheckoutController when 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 27 business days
Printful 25 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

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 page
  • lib/berrypod_web/live/shop/content.ex — replace three PreviewData.*_content() calls with LegalPages.*_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)