berrypod/test/berrypod_web/controllers/unsubscribe_controller_test.exs
jamey 2f4cd81f98 add abandoned cart recovery
When a Stripe checkout session expires without payment, if the customer
entered their email, we record an AbandonedCart and schedule a single
plain-text recovery email (1h delay via Oban).

Privacy design:
- feature is off by default; shop owner opts in via admin settings
- only contacts customers who entered their email at Stripe checkout
- single email, never more (emailed_at timestamp gate)
- suppression list blocks repeat contact; one-click unsubscribe via
  signed token (/unsubscribe/:token)
- records pruned after 30 days (nightly Oban cron)
- no tracking pixels, no redirected links, no HTML

Legal notes:
- custom_text added to Stripe session footer when recovery is on
- UK PECR soft opt-in; EU legitimate interests both satisfied by this design

Files:
- migration: abandoned_carts + email_suppressions tables
- schemas: AbandonedCart, EmailSuppression
- context: Orders.create_abandoned_cart, check_suppression, add_suppression,
  has_recent_paid_order?, get_abandoned_cart_by_session, mark_abandoned_cart_emailed
- workers: AbandonedCartEmailWorker (checkout queue), AbandonedCartPruneWorker (cron)
- notifier: OrderNotifier.deliver_cart_recovery/3
- webhook: extended checkout.session.expired handler
- controller: UnsubscribeController, admin settings toggle
- tests: 28 new tests across context, workers, and controller

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

37 lines
1.1 KiB
Elixir

defmodule BerrypodWeb.UnsubscribeControllerTest do
use BerrypodWeb.ConnCase, async: true
alias Berrypod.Orders
defp sign_token(email) do
Phoenix.Token.sign(BerrypodWeb.Endpoint, "email-unsub", email)
end
describe "GET /unsubscribe/:token" do
test "valid token: suppresses email and returns 200", %{conn: conn} do
token = sign_token("unsub@example.com")
conn = get(conn, ~p"/unsubscribe/#{token}")
assert conn.status == 200
assert conn.resp_body =~ "Unsubscribed"
assert Orders.check_suppression("unsub@example.com") == :suppressed
end
test "invalid token returns 400", %{conn: conn} do
conn = get(conn, ~p"/unsubscribe/bad_token_here")
assert conn.status == 400
assert conn.resp_body =~ "invalid or expired"
end
test "already suppressed — idempotent, still returns 200", %{conn: conn} do
Orders.add_suppression("already@example.com", "unsubscribed")
token = sign_token("already@example.com")
conn = get(conn, ~p"/unsubscribe/#{token}")
assert conn.status == 200
assert Orders.check_suppression("already@example.com") == :suppressed
end
end
end