berrypod/docs/plans/legal-page-generator.md
jamey edef628214 tidy docs: condense progress, trim readme, mark plan statuses
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-01 17:15:18 +00:00

258 lines
12 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Legal page generator
> Status: Complete
> Tasks: #8385
> 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_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
---
### 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>`:
```elixir
[
%{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:
```elixir
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) |