From 933f685b63505fe90d6ad6b6db78162c8f738702 Mon Sep 17 00:00:00 2001 From: jamey Date: Tue, 24 Feb 2026 13:48:49 +0000 Subject: [PATCH] add legal page generator for privacy, delivery, and terms MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replaces hardcoded PreviewData placeholders with generated content derived from real shop state: connected providers (production lead times), shipping countries (grouped by region), shop country (jurisdiction language and governing law), and feature flags (abandoned cart recovery section, newsletter, VAT clause). Returns policy correctly cites Consumer Contracts Regulations Reg 28(1)(b) for POD exemption and Consumer Rights Act for defective goods. Cart recovery section uses jurisdiction-specific wording: PECR Reg 22 for UK, GDPR Art 6(1)(f) for EU, generic otherwise. About page unchanged — shop owner's story to tell. 26 new tests. Co-Authored-By: Claude Sonnet 4.6 --- PROGRESS.md | 10 +- lib/berrypod/legal_pages.ex | 397 +++++++++++++++++++ lib/berrypod_web/live/shop/content.ex | 7 +- test/berrypod/legal_pages_test.exs | 255 ++++++++++++ test/berrypod_web/live/shop/content_test.exs | 9 +- 5 files changed, 665 insertions(+), 13 deletions(-) create mode 100644 lib/berrypod/legal_pages.ex create mode 100644 test/berrypod/legal_pages_test.exs diff --git a/PROGRESS.md b/PROGRESS.md index 1cb825d..032f57c 100644 --- a/PROGRESS.md +++ b/PROGRESS.md @@ -130,12 +130,12 @@ Plans: [admin-redesign.md](docs/plans/admin-redesign.md) | [admin-font-loading.m | | **Other features** | | | | | ~~72~~ | ~~Order status lookup — wire up existing stub on contact page (UI already exists, backend unbuilt)~~ | — | 1.5h | done | | | **Abandoned cart recovery** ([plan](docs/plans/abandoned-cart.md)) | | | | -| 75 | Handle `checkout.session.expired` webhook, store abandoned cart record | — | 1h | planned | -| 76 | Send single recovery email (plain text, no tracking, clear opt-out) | 75 | 1h | planned | -| 77 | Suppression list (unsubscribe), 30-day data pruning Oban job, Stripe footer notice | 76 | 1h | planned | +| ~~75~~ | ~~Handle `checkout.session.expired` webhook, store abandoned cart record~~ | — | 1h | done | +| ~~76~~ | ~~Send single recovery email (plain text, no tracking, clear opt-out)~~ | 75 | 1h | done | +| ~~77~~ | ~~Suppression list (unsubscribe), 30-day data pruning Oban job, Stripe footer notice~~ | 76 | 1h | done | | | **Legal page generator** ([plan](docs/plans/legal-page-generator.md)) | | | | -| 83 | `LegalPages` module — generate accurate privacy, delivery, and terms content from settings + provider + shipping data | — | 2.5h | planned | -| 84 | Wire `LegalPages` into `Content` LiveView — replace `PreviewData` calls, add tests | 83 | 45m | planned | +| ~~83~~ | ~~`LegalPages` module — generate accurate privacy, delivery, and terms content from settings + provider + shipping data~~ | — | 2.5h | done | +| ~~84~~ | ~~Wire `LegalPages` into `Content` LiveView — replace `PreviewData` calls, add tests~~ | 83 | 45m | done | | 85 | Page editor integration — "Regenerate" button, auto-regenerate on settings change, customised vs auto label | 83, 19 | 1.5h | planned | | | **Platform site** | | | | | 73 | Platform/marketing site — brochure, pricing, sign-up | — | TBD | planned | diff --git a/lib/berrypod/legal_pages.ex b/lib/berrypod/legal_pages.ex new file mode 100644 index 0000000..c89f751 --- /dev/null +++ b/lib/berrypod/legal_pages.ex @@ -0,0 +1,397 @@ +defmodule Berrypod.LegalPages do + @moduledoc """ + Generates legally accurate content for the shop's policy pages. + + Content is derived from actual shop settings — connected providers, + shipping destinations, enabled features, and shop country. Each function + returns a list of content blocks in the format used by `<.rich_text>`. + + Not legal advice. All generated pages include a disclaimer. + """ + + alias Berrypod.{Products, Settings, Shipping} + + @eu_countries ~w(AT BE BG CY CZ DE DK EE ES FI FR GR HR HU IE IT LT LU LV MT NL PL PT RO SE SI SK) + @north_america ~w(US CA) + + # ============================================================================= + # Public API + # ============================================================================= + + @doc """ + Generates privacy policy content blocks. + + Sections adapt based on shop country (jurisdiction language), connected + providers (third-party sharing), and enabled features (cart recovery, + newsletter). + """ + 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") + abandoned_cart = Settings.abandoned_cart_recovery_enabled?() + newsletter = Settings.get_setting("newsletter_enabled", false) + providers = connected_provider_names() + + intro = [ + %{ + type: :lead, + text: + "#{shop_name} takes your privacy seriously. This policy explains what personal data we collect and how we use it, #{jurisdiction(shop_country)}." + }, + %{type: :heading, text: "What we collect"}, + %{ + type: :paragraph, + text: + "When you place an order, we collect the information needed to fulfil it: your name, email address, and shipping address. Our lawful basis is performance of a contract (Article 6(1)(b) UK/EU GDPR)." + }, + %{type: :heading, text: "Payment"}, + %{ + type: :paragraph, + text: + "Payment is processed by Stripe. We never see or store your card number or payment details — these go directly to Stripe and are subject to their privacy policy." + }, + %{type: :heading, text: "Analytics"}, + %{ + type: :paragraph, + text: + "We collect privacy-first analytics: page views, device type, browser, operating system, and general country (derived from your IP address at the time of the visit — your IP address itself is not stored). No cookies are used for analytics, and no personal data is retained." + }, + %{type: :heading, text: "Cookies"}, + %{ + type: :list, + items: [ + "_berrypod_session — keeps your shopping cart and login session active. Expires after 7 days or when you log out. Strictly necessary; no consent required.", + "country_code — remembers your shipping country preference. Stored for 1 year. Strictly necessary for showing correct shipping rates; no consent required." + ] + }, + %{ + type: :paragraph, + text: + "We do not use advertising cookies, tracking pixels, or third-party analytics cookies." + } + ] + + cart_recovery_blocks = if abandoned_cart, do: cart_recovery_section(shop_country), else: [] + newsletter_blocks = if newsletter, do: newsletter_section(), else: [] + + retention_items = + [ + "Order data is kept for 7 years (required by UK company law and HMRC accounting rules).", + "Analytics data is retained for 2 years.", + if(abandoned_cart, + do: + "Cart recovery data (email address and basket contents) is deleted within 30 days whether or not an email was sent.", + else: nil + ) + ] + |> Enum.reject(&is_nil/1) + + tail = [ + %{type: :heading, text: "Third parties"}, + %{type: :paragraph, text: provider_sharing_text(providers)}, + %{type: :heading, text: "Data retention"}, + %{type: :list, items: retention_items}, + %{type: :heading, text: "Your rights"}, + %{ + type: :paragraph, + text: + "You have the right to access, correct, or request deletion of your personal data at any time. Note that certain data may need to be retained for the statutory periods above. You also have the right to object to marketing communications." + }, + %{type: :heading, text: "Contact"}, + %{type: :paragraph, text: contact_text(contact_email)}, + %{type: :closing, text: legal_disclaimer()} + ] + + intro ++ cart_recovery_blocks ++ newsletter_blocks ++ tail + end + + @doc """ + Generates delivery & returns content blocks. + + Production lead times are derived from connected providers. Shipping + destinations are grouped by region from the shipping_rates table. + Returns policy cites the correct UK legal exemptions for POD. + """ + def delivery_content do + providers = connected_providers() + countries = Shipping.list_available_countries_with_names() + contact_email = Settings.get_setting("contact_email") + + [ + %{ + type: :lead, + text: + "All products are printed on demand and dispatched directly from our print provider. Here's everything you need to know about delivery and returns." + }, + %{type: :heading, text: "Production time"}, + %{type: :paragraph, text: production_lead_time(providers)}, + %{type: :heading, text: "Delivery"} + ] ++ + shipping_region_blocks(countries) ++ + [ + %{type: :heading, text: "Returns & exchanges"}, + %{ + type: :paragraph, + text: + "Because every item is made to order, we cannot accept returns for change of mind. Under the Consumer Contracts (Information, Cancellation and Additional Charges) Regulations 2013, Regulation 28(1)(b), the 14-day cancellation right does not apply to goods made to the consumer's specifications or clearly personalised. Every product in this shop qualifies for this exemption." + }, + %{ + type: :paragraph, + text: + "Your rights under the Consumer Rights Act 2015 are unaffected. If your order arrives damaged or with a printing defect, you're entitled to a repair, replacement, or refund. Contact us within 14 days of receiving your order with your order number and a photo of the issue." + }, + %{type: :heading, text: "Cancellations"}, + %{type: :paragraph, text: cancellation_text(contact_email)}, + %{type: :closing, text: legal_disclaimer()} + ] + end + + @doc """ + Generates terms of service content blocks. + + Governing law is derived from shop country. VAT clause included only + when the vat_enabled setting is true. + """ + def terms_content do + shop_name = Settings.get_setting("shop_name", "this shop") + shop_country = Settings.get_setting("shop_country", "GB") + vat_enabled = Settings.get_setting("vat_enabled", false) + law = governing_law(shop_country) + + payment_text = + "Payment is taken at the time of purchase via Stripe. Your order is confirmed only on successful payment." <> + if(vat_enabled, do: " Prices include VAT where applicable.", else: "") + + [ + %{ + type: :lead, + text: + "By placing an order through #{shop_name}, you agree to these terms. These terms are governed by #{law}." + }, + %{type: :heading, text: "Products"}, + %{ + type: :paragraph, + text: + "All products are made to order using print-on-demand services. Colours may vary slightly between screen and the finished print — minor differences are normal. We do our best to represent products accurately in photos and descriptions." + }, + %{type: :heading, text: "Payment"}, + %{type: :paragraph, text: payment_text}, + %{type: :heading, text: "Delivery"}, + %{ + type: :paragraph, + text: + "Orders include a production period before dispatch. Delivery times are estimates and may vary. We are not liable for delays caused by print providers or postal services. See our delivery page for current timeframes." + }, + %{type: :heading, text: "Returns"}, + %{ + type: :paragraph, + text: + "As all items are made to order, we do not accept returns for change of mind — this falls under the Consumer Contracts (Information, Cancellation and Additional Charges) Regulations 2013, Regulation 28(1)(b). If an item arrives damaged or defective, you are entitled to a remedy under the Consumer Rights Act 2015. See our delivery and returns page for the full process." + }, + %{type: :heading, text: "Intellectual property"}, + %{ + type: :paragraph, + text: + "All designs, images, and content on this site are the property of #{shop_name} and may not be reproduced without permission. Purchasing a product grants a personal-use licence only." + }, + %{type: :heading, text: "Liability"}, + %{ + type: :paragraph, + text: + "Our liability is limited to the value of your order. We are not responsible for delays or errors caused by print providers, postal services, or other third parties beyond our reasonable control." + }, + %{type: :heading, text: "Governing law"}, + %{ + type: :paragraph, + text: + "These terms are governed by #{law}. Any dispute will be subject to the exclusive jurisdiction of the relevant courts." + }, + %{type: :heading, text: "Changes"}, + %{ + type: :paragraph, + text: + "We may update these terms from time to time. Any changes apply to orders placed after the update. The current version is always available at this page." + }, + %{type: :closing, text: legal_disclaimer()} + ] + end + + # ============================================================================= + # Private helpers + # ============================================================================= + + defp connected_providers do + Products.list_provider_connections() + |> Enum.filter(& &1.enabled) + end + + defp connected_provider_names do + connected_providers() + |> Enum.map(fn conn -> + case conn.provider_type do + "printify" -> "Printify" + "printful" -> "Printful" + _ -> conn.name || "our print provider" + end + end) + end + + defp jurisdiction("GB"), + do: "under UK GDPR and the Privacy and Electronic Communications Regulations (PECR)" + + defp jurisdiction(code) when code in @eu_countries, + do: "under the EU General Data Protection Regulation (GDPR)" + + defp jurisdiction(_), do: "under applicable data protection laws" + + defp governing_law("GB"), do: "English law" + + defp governing_law(code) when code in @eu_countries do + name = Shipping.country_name(code) + "the law of #{name} and EU regulations" + end + + defp governing_law(code) do + name = Shipping.country_name(code) + if name == code, do: "applicable local law", else: "the law of #{name}" + end + + defp production_lead_time([]) do + "Orders are printed on demand and typically take several business days to produce before dispatch. Production time varies by product." + end + + defp production_lead_time(connections) do + types = MapSet.new(connections, & &1.provider_type) + + cond do + MapSet.member?(types, "printify") and MapSet.member?(types, "printful") -> + "Orders are printed on demand. Production time varies by product: Printify items typically take 2–7 business days, Printful items typically take 2–5 business days." + + MapSet.member?(types, "printify") -> + "Orders are printed on demand and typically take 2–7 business days to produce before dispatch." + + MapSet.member?(types, "printful") -> + "Orders are printed on demand and typically take 2–5 business days to produce before dispatch." + + true -> + "Orders are printed on demand and typically take several business days to produce before dispatch." + end + end + + defp shipping_region_blocks([]) do + [ + %{ + type: :paragraph, + text: + "We ship worldwide. Contact us for current delivery time estimates for your location." + } + ] + end + + defp shipping_region_blocks(country_tuples) do + codes = Enum.map(country_tuples, &elem(&1, 0)) + known = ["GB"] ++ @eu_countries ++ @north_america + + items = + [ + "GB" in codes && "United Kingdom: typically 1–3 business days after dispatch", + Enum.any?(codes, &(&1 in @eu_countries)) && + "Europe: typically 3–7 business days after dispatch", + Enum.any?(codes, &(&1 in @north_america)) && + "United States and Canada: typically 5–10 business days after dispatch", + Enum.any?(codes, &(&1 not in known)) && + "Rest of world: typically 7–21 business days after dispatch" + ] + |> Enum.filter(&(&1 != false)) + + [ + %{type: :list, items: items}, + %{ + type: :paragraph, + text: + "Delivery times are estimates and are in addition to the production time above. You'll receive tracking information once your order is dispatched." + } + ] + end + + defp cart_recovery_section("GB") do + [ + %{type: :heading, text: "Cart recovery"}, + %{ + type: :paragraph, + text: + "If you enter your email address during checkout but don't complete your order, we may send you one follow-up email about the items in your basket. We do this under the soft opt-in rule in the Privacy and Electronic Communications Regulations (PECR), Regulation 22, which permits a single 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 in that email. We store your email address and basket contents for this purpose only, and delete them within 30 days whether or not an email was sent. No tracking pixels or click-tracking links are used in these emails." + } + ] + end + + defp cart_recovery_section(code) when code in @eu_countries do + [ + %{type: :heading, text: "Cart recovery"}, + %{ + type: :paragraph, + text: + "If you enter your email address during checkout but don't complete your order, we may send you one follow-up email about the items in your basket. Our lawful basis is legitimate interests (EU GDPR Article 6(1)(f)) — 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. No tracking pixels or click-tracking links are used in these emails." + } + ] + end + + defp cart_recovery_section(_) do + [ + %{type: :heading, text: "Cart recovery"}, + %{ + type: :paragraph, + text: + "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. No tracking pixels or click-tracking links are used in these emails." + } + ] + end + + defp newsletter_section do + [ + %{type: :heading, text: "Newsletter"}, + %{ + type: :paragraph, + text: + "If you subscribe to our newsletter, we use your email address to send marketing emails about new products and offers. We'll only contact you if you've opted in. You can unsubscribe at any time using the link in any email we send." + } + ] + end + + defp provider_sharing_text([]) do + "To fulfil your order, we share your name and shipping address with our print-on-demand provider. Payment details are handled by Stripe. Both process data in accordance with their own privacy policies." + end + + defp provider_sharing_text([name]) do + "To fulfil your order, we share your name and shipping address with #{name}. Payment details are handled by Stripe. Both process data in accordance with their own privacy policies." + end + + defp provider_sharing_text(names) do + joined = Enum.join(names, " and ") + + "To fulfil your order, we share your name and shipping address with #{joined}. Payment details are handled by Stripe. All process data in accordance with their own privacy policies." + end + + defp contact_text(nil), + do: "For any data-related questions or requests, get in touch via our contact page." + + defp contact_text(""), do: contact_text(nil) + + defp contact_text(email), + do: "For any data-related questions or requests, contact us at #{email}." + + defp cancellation_text(nil), + do: + "Orders can be cancelled within approximately 2 hours of being placed, before production begins. Contact us via our contact page as soon as possible if you need to cancel." + + defp cancellation_text(""), do: cancellation_text(nil) + + defp cancellation_text(email), + do: + "Orders can be cancelled within approximately 2 hours of being placed, before production begins. Get in touch at #{email} as soon as possible if you need to cancel." + + defp legal_disclaimer do + "This page was generated from this shop's configuration. It's a good starting point — review it and take independent legal advice if you're unsure about anything." + end +end diff --git a/lib/berrypod_web/live/shop/content.ex b/lib/berrypod_web/live/shop/content.ex index e4719d9..3f19e76 100644 --- a/lib/berrypod_web/live/shop/content.ex +++ b/lib/berrypod_web/live/shop/content.ex @@ -1,6 +1,7 @@ defmodule BerrypodWeb.Shop.Content do use BerrypodWeb, :live_view + alias Berrypod.LegalPages alias Berrypod.Theme.PreviewData @impl true @@ -44,7 +45,7 @@ defmodule BerrypodWeb.Shop.Content do active_page: "delivery", hero_title: "Delivery & returns", hero_description: "Everything you need to know about shipping and returns", - content_blocks: PreviewData.delivery_content() + content_blocks: LegalPages.delivery_content() } end @@ -56,7 +57,7 @@ defmodule BerrypodWeb.Shop.Content do active_page: "privacy", hero_title: "Privacy policy", hero_description: "How we handle your personal information", - content_blocks: PreviewData.privacy_content() + content_blocks: LegalPages.privacy_content() } end @@ -68,7 +69,7 @@ defmodule BerrypodWeb.Shop.Content do active_page: "terms", hero_title: "Terms of service", hero_description: "The legal bits", - content_blocks: PreviewData.terms_content() + content_blocks: LegalPages.terms_content() } end end diff --git a/test/berrypod/legal_pages_test.exs b/test/berrypod/legal_pages_test.exs new file mode 100644 index 0000000..508e93a --- /dev/null +++ b/test/berrypod/legal_pages_test.exs @@ -0,0 +1,255 @@ +defmodule Berrypod.LegalPagesTest do + use Berrypod.DataCase, async: true + + alias Berrypod.LegalPages + alias Berrypod.Settings + + # --------------------------------------------------------------------------- + # Helpers + # --------------------------------------------------------------------------- + + defp has_heading?(blocks, text) do + Enum.any?(blocks, &(&1.type == :heading and &1.text == text)) + end + + defp has_text_containing?(blocks, fragment) do + Enum.any?(blocks, fn block -> + case block do + %{text: text} when is_binary(text) -> String.contains?(text, fragment) + %{items: items} -> Enum.any?(items, &String.contains?(&1, fragment)) + _ -> false + end + end) + end + + # --------------------------------------------------------------------------- + # privacy_content/0 + # --------------------------------------------------------------------------- + + describe "privacy_content/0" do + test "returns a list of blocks with :lead as the first" do + blocks = LegalPages.privacy_content() + + assert is_list(blocks) + assert hd(blocks).type == :lead + end + + test "last block is a :closing disclaimer" do + blocks = LegalPages.privacy_content() + + assert List.last(blocks).type == :closing + assert String.contains?(List.last(blocks).text, "legal advice") + end + + test "always includes core sections" do + blocks = LegalPages.privacy_content() + + assert has_heading?(blocks, "What we collect") + assert has_heading?(blocks, "Payment") + assert has_heading?(blocks, "Cookies") + assert has_heading?(blocks, "Third parties") + assert has_heading?(blocks, "Your rights") + assert has_heading?(blocks, "Contact") + end + + test "includes PECR cart recovery wording for GB shop" do + Settings.put_setting("shop_country", "GB") + Settings.set_abandoned_cart_recovery(true) + + blocks = LegalPages.privacy_content() + + assert has_heading?(blocks, "Cart recovery") + assert has_text_containing?(blocks, "PECR") + assert has_text_containing?(blocks, "Regulation 22") + end + + test "includes GDPR legitimate interests wording for EU shop" do + Settings.put_setting("shop_country", "DE") + Settings.set_abandoned_cart_recovery(true) + + blocks = LegalPages.privacy_content() + + assert has_heading?(blocks, "Cart recovery") + assert has_text_containing?(blocks, "Article 6(1)(f)") + refute has_text_containing?(blocks, "PECR") + end + + test "includes generic cart recovery wording for non-UK/EU shop" do + Settings.put_setting("shop_country", "US") + Settings.set_abandoned_cart_recovery(true) + + blocks = LegalPages.privacy_content() + + assert has_heading?(blocks, "Cart recovery") + refute has_text_containing?(blocks, "PECR") + refute has_text_containing?(blocks, "Article 6(1)(f)") + end + + test "omits cart recovery section when feature is disabled" do + Settings.set_abandoned_cart_recovery(false) + + blocks = LegalPages.privacy_content() + + refute has_heading?(blocks, "Cart recovery") + end + + test "includes newsletter section when enabled" do + Settings.put_setting("newsletter_enabled", true, "boolean") + + blocks = LegalPages.privacy_content() + + assert has_heading?(blocks, "Newsletter") + end + + test "omits newsletter section when disabled" do + Settings.put_setting("newsletter_enabled", false, "boolean") + + blocks = LegalPages.privacy_content() + + refute has_heading?(blocks, "Newsletter") + end + + test "includes jurisdiction language in lead for GB" do + Settings.put_setting("shop_country", "GB") + + blocks = LegalPages.privacy_content() + + assert has_text_containing?(blocks, "UK GDPR") + assert has_text_containing?(blocks, "PECR") + end + + test "includes GDPR jurisdiction language for EU country" do + Settings.put_setting("shop_country", "FR") + + blocks = LegalPages.privacy_content() + + assert has_text_containing?(blocks, "EU General Data Protection Regulation") + end + + test "includes contact email when set" do + Settings.put_setting("contact_email", "hello@example.com") + + blocks = LegalPages.privacy_content() + + assert has_text_containing?(blocks, "hello@example.com") + end + end + + # --------------------------------------------------------------------------- + # delivery_content/0 + # --------------------------------------------------------------------------- + + describe "delivery_content/0" do + test "returns a list of blocks with :lead as the first" do + blocks = LegalPages.delivery_content() + + assert is_list(blocks) + assert hd(blocks).type == :lead + end + + test "always includes returns, cancellations, and production sections" do + blocks = LegalPages.delivery_content() + + assert has_heading?(blocks, "Production time") + assert has_heading?(blocks, "Returns & exchanges") + assert has_heading?(blocks, "Cancellations") + end + + test "cites Consumer Contracts Regulations in returns section" do + blocks = LegalPages.delivery_content() + + assert has_text_containing?(blocks, "Consumer Contracts") + assert has_text_containing?(blocks, "Regulation 28(1)(b)") + end + + test "cites Consumer Rights Act for defective goods" do + blocks = LegalPages.delivery_content() + + assert has_text_containing?(blocks, "Consumer Rights Act 2015") + end + + test "handles empty shipping countries gracefully" do + # No shipping rates seeded — list will be empty in test + blocks = LegalPages.delivery_content() + + # Should not crash and should include a fallback paragraph + assert is_list(blocks) + assert has_text_containing?(blocks, "worldwide") + end + + test "last block is a :closing disclaimer" do + blocks = LegalPages.delivery_content() + + assert List.last(blocks).type == :closing + end + end + + # --------------------------------------------------------------------------- + # terms_content/0 + # --------------------------------------------------------------------------- + + describe "terms_content/0" do + test "returns a list of blocks with :lead as the first" do + blocks = LegalPages.terms_content() + + assert is_list(blocks) + assert hd(blocks).type == :lead + end + + test "always includes core sections" do + blocks = LegalPages.terms_content() + + assert has_heading?(blocks, "Products") + assert has_heading?(blocks, "Payment") + assert has_heading?(blocks, "Returns") + assert has_heading?(blocks, "Governing law") + assert has_heading?(blocks, "Changes") + end + + test "governing law is English law for GB shop" do + Settings.put_setting("shop_country", "GB") + + blocks = LegalPages.terms_content() + + assert has_text_containing?(blocks, "English law") + end + + test "governing law references EU regulations for EU shop" do + Settings.put_setting("shop_country", "DE") + + blocks = LegalPages.terms_content() + + assert has_text_containing?(blocks, "EU regulations") + end + + test "includes VAT clause when vat_enabled is true" do + Settings.put_setting("vat_enabled", true, "boolean") + + blocks = LegalPages.terms_content() + + assert has_text_containing?(blocks, "VAT") + end + + test "omits VAT clause when vat_enabled is false" do + Settings.put_setting("vat_enabled", false, "boolean") + + blocks = LegalPages.terms_content() + + refute has_text_containing?(blocks, "VAT") + end + + test "includes shop name in lead" do + Settings.put_setting("shop_name", "Petal & Ink") + + blocks = LegalPages.terms_content() + + assert has_text_containing?(blocks, "Petal & Ink") + end + + test "last block is a :closing disclaimer" do + blocks = LegalPages.terms_content() + + assert List.last(blocks).type == :closing + end + end +end diff --git a/test/berrypod_web/live/shop/content_test.exs b/test/berrypod_web/live/shop/content_test.exs index 93a4555..a66e2c0 100644 --- a/test/berrypod_web/live/shop/content_test.exs +++ b/test/berrypod_web/live/shop/content_test.exs @@ -36,16 +36,15 @@ defmodule BerrypodWeb.Shop.ContentTest do test "displays delivery content", %{conn: conn} do {:ok, _view, html} = live(conn, ~p"/delivery") - assert html =~ "Shipping" + assert html =~ "Production time" assert html =~ "Returns & exchanges" assert html =~ "Cancellations" end - test "displays list items", %{conn: conn} do + test "displays shipping fallback when no rates are configured", %{conn: conn} do {:ok, _view, html} = live(conn, ~p"/delivery") - assert html =~ "United Kingdom" - assert html =~ "5–8 business days" + assert html =~ "worldwide" end end @@ -78,7 +77,7 @@ defmodule BerrypodWeb.Shop.ContentTest do {:ok, _view, html} = live(conn, ~p"/terms") assert html =~ "Products" - assert html =~ "Orders & payment" + assert html =~ "Payment" assert html =~ "Intellectual property" end end