From 255912af73f68b2c888f1db1580986ba51def970 Mon Sep 17 00:00:00 2001 From: jamey Date: Fri, 13 Mar 2026 13:34:36 +0000 Subject: [PATCH] update docs and progress tracking Co-Authored-By: Claude Opus 4.5 --- .envrc.example | 11 + PROGRESS.md | 47 ++ README.md | 54 ++ docs/plans/competitive-gap-analysis.md | 2 +- docs/plans/competitive-gaps.md | 705 +++++++++++++++++++++++++ 5 files changed, 818 insertions(+), 1 deletion(-) create mode 100644 .envrc.example create mode 100644 docs/plans/competitive-gaps.md diff --git a/.envrc.example b/.envrc.example new file mode 100644 index 0000000..8bc5751 --- /dev/null +++ b/.envrc.example @@ -0,0 +1,11 @@ +# direnv configuration for Berrypod development +# Copy to .envrc and run `direnv allow` to activate + +# SQLCipher build flags (required after installing libsqlcipher-dev) +# These tell exqlite to use system SQLCipher instead of bundled SQLite +export EXQLITE_USE_SYSTEM=1 +export EXQLITE_SYSTEM_CFLAGS="-I/usr/include/sqlcipher" +export EXQLITE_SYSTEM_LDFLAGS="-lsqlcipher" + +# Optional: enable database encryption in dev (omit for unencrypted) +# export SECRET_KEY_DB="dev-only-key-not-for-production" diff --git a/PROGRESS.md b/PROGRESS.md index 186cf19..7f0c03a 100644 --- a/PROGRESS.md +++ b/PROGRESS.md @@ -103,6 +103,52 @@ Extend the existing page editor (PageEditorHook + editor_sheet) to include theme |---|---|---|---| | 1 | Fix double radio button dots in theme editor | 30m | done | +### Competitive gaps ([plan](docs/plans/competitive-gaps.md)) + +Close critical gaps identified in the competitive analysis. Phased approach: core commerce first, then retention/growth, then scale. + +**Phase 1: Core commerce gaps** + +| # | Task | Depends on | Est | Status | +|---|------|------------|-----|--------| +| 75 | Customer authentication schema | — | 2h | planned | +| 76 | Customer auth flows (login, register, reset) | 75 | 3h | planned | +| 77 | Link orders to customers | 75, 76 | 1.5h | planned | +| 78 | Customer account dashboard | 76 | 2h | planned | +| 79 | Saved addresses | 76 | 1.5h | planned | +| 80 | Guest checkout linking | 75, 76 | 1h | planned | +| 81 | PayPal SDK integration | — | 2h | planned | +| 82 | PayPal checkout flow | 81 | 3h | planned | +| 83 | PayPal webhooks | 82 | 1.5h | planned | +| 84 | Reviews schema | — | 1.5h | planned | +| 85 | Review submission | 84 | 2h | planned | +| 86 | Review moderation | 84 | 1.5h | planned | +| 87 | Reviews display | 84 | 1.5h | planned | +| 88 | Review schema markup | 87 | 1h | planned | +| 89 | Provider stock sync | — | — | done (already existed) | +| 90 | Availability display | 89 | 1h | done | + +**Phase 2: Retention & growth** + +| # | Task | Depends on | Est | Status | +|---|------|------------|-----|--------| +| 91 | Returns schema | — | 1.5h | planned | +| 92 | Return request flow | 91, 78 | 2h | planned | +| 93 | Return admin | 91 | 2h | planned | +| 94 | Return policy settings | 91 | 1h | planned | +| 95 | Email sequence schema | — | 2h | planned | +| 96 | Sequence triggers & sending | 95 | 3h | planned | +| 97 | Sequence admin | 95 | 2h | planned | +| 98 | Customer data export (GDPR) | 75 | 1.5h | planned | +| 99 | Customer data deletion (GDPR) | 75 | 2h | planned | + +**Phase 3: Scale** + +| # | Task | Depends on | Est | Status | +|---|------|------------|-----|--------| +| 100 | Blog post type | — | 3h | planned | +| 101 | Staff accounts & RBAC | — | 4h | planned | + ### Editor panel reorganisation ([plan](docs/plans/editor-reorganisation.md)) Restructure the 3-tab editor panel for better discoverability. Replace Settings tab with Site tab for site-wide content (announcement bar text, social links, nav items, footer content). Move branding from Theme to Site. Merge page settings inline into Page tab. @@ -195,3 +241,4 @@ All plans in [docs/plans/](docs/plans/). Completed plans are kept as architectur | [editor-reorganisation.md](docs/plans/editor-reorganisation.md) | Planned | | [seo-enhancements.md](docs/plans/seo-enhancements.md) | Planned | | [competitive-gap-analysis.md](docs/plans/competitive-gap-analysis.md) | Reference | +| [competitive-gaps.md](docs/plans/competitive-gaps.md) | Planned | diff --git a/README.md b/README.md index ef7ac6b..ac192f2 100644 --- a/README.md +++ b/README.md @@ -30,6 +30,7 @@ Complete storefront with all the pages you need: ### Technical highlights - Hand-written CSS with three-layer architecture (9.8 KB gzipped shop, 17.8 KB admin) - SQLite with BLOB storage, IMMEDIATE transactions, WAL, mmap +- SQLCipher encryption at rest (AES-256, optional for dev, required for prod) - Image optimisation pipeline (AVIF/WebP/JPEG responsive variants via Oban) - ETS caching for CSS, pages, redirects, favicons - 99-100 PageSpeed mobile, no-JS support across all key flows @@ -102,6 +103,59 @@ assets/css/ └── theme-layer3-semantic.css # component styles ``` +## Database encryption + +Berrypod uses SQLCipher to encrypt the entire SQLite database at rest. Two independent secrets provide defence in depth: + +| Secret | Purpose | +|--------|---------| +| `SECRET_KEY_BASE` | Phoenix sessions, Cloak field encryption | +| `SECRET_KEY_DB` | SQLCipher whole-database encryption | + +### Development + +Encryption is optional for development. To test locally with encryption: + +```bash +# Generate a key (hex-only recommended) +openssl rand -hex 32 + +# Set environment variable +export SECRET_KEY_DB="your-hex-key" + +# Recreate database with encryption +mix ecto.reset +mix phx.server +``` + +Without `SECRET_KEY_DB`, the database is unencrypted. + +### Production + +Both secrets are required. Generate them: + +```bash +mix phx.gen.secret # → SECRET_KEY_BASE +openssl rand -hex 32 # → SECRET_KEY_DB (or mix phx.gen.secret) +``` + +For Fly.io deployment: +```bash +fly secrets set SECRET_KEY_BASE="..." SECRET_KEY_DB="..." +``` + +### Backup and restore + +Admin > Backup provides: +- Database stats (size, encryption status, table breakdown) +- Download backup (encrypted with same key) +- Restore from backup (validates key matches) + +**Key management:** +- Lost key = lost data. No recovery possible. +- Store keys securely (password manager, secrets manager). +- Backups are portable — copy file + set same key = working shop. + ## Stripe setup 1. Create a [Stripe account](https://dashboard.stripe.com/register) diff --git a/docs/plans/competitive-gap-analysis.md b/docs/plans/competitive-gap-analysis.md index 0388c11..26be48c 100644 --- a/docs/plans/competitive-gap-analysis.md +++ b/docs/plans/competitive-gap-analysis.md @@ -1,6 +1,6 @@ # Competitive Gap Analysis -Status: Reference +Status: Reference (implementation plan: [competitive-gaps.md](competitive-gaps.md)) ## Overview diff --git a/docs/plans/competitive-gaps.md b/docs/plans/competitive-gaps.md new file mode 100644 index 0000000..fdf66b9 --- /dev/null +++ b/docs/plans/competitive-gaps.md @@ -0,0 +1,705 @@ +# Competitive gaps + +> Status: Planned +> Tasks: #75–101 in PROGRESS.md +> Tier: 4.5 (Core commerce gaps) + +## Goal + +Close the critical gaps identified in the competitive analysis that block Berrypod from competing with Shopify, Squarespace, and established POD platforms. + +## Phasing + +The gaps are grouped into three phases, ordered by impact: + +1. **Phase 1 (Core commerce)** — unblocks repeat customers and essential e-commerce features +2. **Phase 2 (Retention & growth)** — keeps customers coming back and scales operations +3. **Phase 3 (Scale)** — differentiators and advanced features + +Items already planned elsewhere (profit-aware-pricing, SEO enhancements) are not duplicated here. + +--- + +## Phase 1: Core commerce gaps + +### Customer accounts (#75–80) + +The biggest unlock. Returning customers can't see past orders, save addresses, or leave reviews without accounts. + +#### #75 — Customer authentication schema (2h) + +New table and auth infrastructure for customer accounts (separate from admin users). + +**Schema:** +```elixir +create table(:customers, primary_key: false) do + add :id, :binary_id, primary_key: true + add :email, :citext, null: false + add :hashed_password, :string, null: false + add :confirmed_at, :utc_datetime + add :name, :string + timestamps() +end + +create unique_index(:customers, [:email]) +``` + +**Files:** +- Migration +- `lib/berrypod/customers/customer.ex` — schema + registration/password changesets +- `lib/berrypod/customers.ex` — context (register, authenticate, get_by_email) + +**Tests:** +- Registration with valid/invalid data +- Authentication with correct/wrong password +- Email uniqueness + +--- + +#### #76 — Customer auth flows (3h) + +Login, register, password reset, email confirmation for shop customers. + +**Routes:** +- `POST /account/register` — create account +- `POST /account/login` — log in +- `DELETE /account/logout` — log out +- `GET /account/confirm/:token` — email confirmation +- `POST /account/reset-password` — request reset +- `PUT /account/reset-password/:token` — set new password + +**Sessions:** +- Customer session separate from admin session (different cookie) +- `@current_customer` assign in shop live_session + +**UI:** +- Register/login forms (modal or page TBD) +- Email templates for confirmation and reset +- "Already have an account?" / "Create account" links + +**Files:** +- `lib/berrypod_web/controllers/customer_session_controller.ex` +- `lib/berrypod_web/controllers/customer_registration_controller.ex` +- `lib/berrypod_web/live/shop/customer_auth_live.ex` — optional LiveView forms +- Email templates +- Router updates + +**No-JS:** Forms work as standard POST. + +--- + +#### #77 — Link orders to customers (1.5h) + +Associate orders with customer accounts. + +**Migration:** +```elixir +alter table(:orders) do + add :customer_id, references(:customers, type: :binary_id, on_delete: :nilify_all) +end + +create index(:orders, [:customer_id]) +``` + +**Flow:** +- At checkout, if `@current_customer` exists, set `customer_id` on order +- Guest checkout still works (customer_id = nil) +- "Create account?" prompt after guest checkout (pre-fill email from order) + +**Files:** +- Migration +- `lib/berrypod/orders/order.ex` — add field +- Checkout controller — set customer_id +- Post-checkout prompt component + +--- + +#### #78 — Customer account dashboard (2h) + +`/account` page for logged-in customers. + +**Features:** +- Order history list (status, date, total, tracking link) +- Click into order detail (items, addresses, timeline) +- Account settings (name, email, password) + +**Files:** +- `lib/berrypod_web/live/shop/account_live.ex` +- `lib/berrypod_web/live/shop/account_orders_live.ex` +- Router — `/account`, `/account/orders/:id` + +--- + +#### #79 — Saved addresses (1.5h) + +Address book for returning customers. + +**Schema:** +```elixir +create table(:customer_addresses, primary_key: false) do + add :id, :binary_id, primary_key: true + add :customer_id, references(:customers, type: :binary_id, on_delete: :delete_all), null: false + add :label, :string # "Home", "Work", etc. + add :name, :string, null: false + add :line1, :string, null: false + add :line2, :string + add :city, :string, null: false + add :state, :string + add :postal_code, :string, null: false + add :country, :string, null: false # ISO code + add :is_default, :boolean, default: false + timestamps() +end +``` + +**Features:** +- Add/edit/delete addresses in account settings +- Address selector at checkout (for logged-in customers) +- "Save this address" checkbox at checkout +- Prefill Stripe Checkout with selected address + +**Files:** +- Migration + schema +- `lib/berrypod/customers.ex` — address CRUD +- Account settings component +- Checkout integration + +--- + +#### #80 — Guest checkout linking (1h) + +After guest checkout, prompt to create account and link the order. + +**Flow:** +1. Order confirmation page shows "Create account to track orders" +2. Email pre-filled from order +3. On registration, link order to new customer + +**Files:** +- Order confirmation component +- Registration flow — accept order_id param, link on create + +--- + +### PayPal integration (#81–83) + +~30% of buyers expect PayPal. Critical for conversions. + +#### #81 — PayPal SDK integration (2h) + +Set up PayPal JavaScript SDK and server-side API. + +**Dependencies:** +- PayPal REST API (use `:req` for HTTP) +- Client ID + Secret in encrypted settings + +**Files:** +- `lib/berrypod/payments/paypal.ex` — API client (create order, capture payment) +- Settings — PayPal credentials +- Admin settings UI — PayPal section + +--- + +#### #82 — PayPal checkout flow (3h) + +Add PayPal as payment option alongside Stripe. + +**Flow:** +1. Cart page shows "Pay with PayPal" button (PayPal JS SDK) +2. Customer clicks → PayPal modal opens +3. Customer approves → returns to site +4. Server captures payment +5. Order created/confirmed + +**Approach:** +- PayPal handles the entire payment UI (like Stripe Checkout) +- Server creates PayPal order, client approves, server captures +- Same order creation flow as Stripe, different payment capture + +**Files:** +- `lib/berrypod_web/controllers/paypal_controller.ex` — create order, capture +- Cart/checkout UI — PayPal button +- JS — PayPal SDK integration +- Order creation — handle PayPal payments + +--- + +#### #83 — PayPal webhooks (1.5h) + +Handle PayPal webhooks for refunds and disputes. + +**Events:** +- `PAYMENT.CAPTURE.REFUNDED` — mark order refunded +- `CUSTOMER.DISPUTE.CREATED` — flag order + +**Files:** +- `lib/berrypod_web/controllers/paypal_webhook_controller.ex` +- Router — `/webhooks/paypal` +- Orders context — refund/dispute handling + +--- + +### Product reviews (#84–88) + +Reviews block exists but has no backend. Critical for social proof. + +#### #84 — Reviews schema (1.5h) + +**Schema:** +```elixir +create table(:reviews, primary_key: false) do + add :id, :binary_id, primary_key: true + add :product_id, references(:products, type: :binary_id, on_delete: :delete_all), null: false + add :customer_id, references(:customers, type: :binary_id, on_delete: :nilify_all) + add :order_id, references(:orders, type: :binary_id, on_delete: :nilify_all) + add :rating, :integer, null: false # 1-5 + add :title, :string + add :body, :text + add :status, :string, default: "pending" # pending, approved, rejected + add :verified_purchase, :boolean, default: false + add :author_name, :string # for guest reviews or if customer deleted + timestamps() +end + +create index(:reviews, [:product_id]) +create index(:reviews, [:customer_id]) +create index(:reviews, [:status]) +``` + +**Files:** +- Migration +- `lib/berrypod/reviews/review.ex` +- `lib/berrypod/reviews.ex` — context + +--- + +#### #85 — Review submission (2h) + +Customers can leave reviews on purchased products. + +**Routes:** +- `POST /products/:id/reviews` — submit review + +**Rules:** +- Must have purchased the product (order with this product, status paid+) +- One review per customer per product +- Optional: allow guest reviews with email verification + +**UI:** +- Review form on product page (only for purchasers) +- Star rating selector +- Optional title and body + +**Files:** +- `lib/berrypod_web/live/shop/product_live.ex` — review form component +- Review creation with validation + +--- + +#### #86 — Review moderation (1.5h) + +Admin can approve/reject reviews before display. + +**Admin UI:** +- `/admin/reviews` — pending reviews queue +- Approve/reject buttons +- Bulk actions + +**Moderation:** +- `status: pending` by default +- Only `approved` reviews shown on product pages +- Email notification to admin on new review (optional) + +**Files:** +- `lib/berrypod_web/live/admin/reviews_live.ex` +- Router +- Admin nav + +--- + +#### #87 — Reviews display (1.5h) + +Show approved reviews on product pages. + +**Features:** +- Average rating + count in product header +- Review list below product details +- "Verified purchase" badge +- Sort by date/rating +- Pagination + +**Files:** +- Product page — reviews section +- `lib/berrypod/reviews.ex` — queries (avg rating, list for product) +- Shop components — review card, star display + +--- + +#### #88 — Review schema markup (1h) + +Add JSON-LD `Review` and `AggregateRating` schema. + +**Schema:** +```json +{ + "@type": "Product", + "aggregateRating": { + "@type": "AggregateRating", + "ratingValue": "4.5", + "reviewCount": "12" + }, + "review": [...] +} +``` + +**Files:** +- Product page — extend existing JSON-LD +- `lib/berrypod_web/components/seo_components.ex` + +--- + +### Stock status improvements (#89–90) — Complete + +Accurate availability status from providers. No artificial urgency — POD products are made to order. Stock limits reflect provider availability, not scarcity marketing. + +#### #89 — Provider stock sync — Already exists + +The `is_available` field already exists on `ProductVariant` and is synced from both providers: +- Printify: `var["is_available"]` +- Printful: `sv["availability_status"] == "active"` + +Product-level `in_stock` is computed from variant availability in `Products.recompute_cached_fields/1`. + +#### #90 — Availability display — Complete + +**Implemented:** +- Unavailable variants are visually disabled in the variant selector (opacity 0.3, dashed border) +- "This option is currently unavailable" message shown when an unavailable variant is selected +- Add-to-cart button disabled for unavailable variants +- Cart shows "This item is currently unavailable" warning for affected items +- Checkout blocked if cart contains unavailable items + +**No "X left" urgency** — that's a dark pattern for made-to-order products. + +--- + +--- + +## Phase 2: Retention & growth + +### Returns management (#91–94) + +No RMA system currently. Customers can't request returns. + +#### #91 — Returns schema (1.5h) + +**Schema:** +```elixir +create table(:return_requests, primary_key: false) do + add :id, :binary_id, primary_key: true + add :order_id, references(:orders, type: :binary_id, on_delete: :delete_all), null: false + add :customer_id, references(:customers, type: :binary_id, on_delete: :nilify_all) + add :status, :string, default: "requested" # requested, approved, rejected, completed + add :reason, :string, null: false + add :notes, :text + add :refund_amount, :integer # approved refund amount + timestamps() +end + +create table(:return_request_items, primary_key: false) do + add :id, :binary_id, primary_key: true + add :return_request_id, references(:return_requests, type: :binary_id, on_delete: :delete_all) + add :order_item_id, references(:order_items, type: :binary_id, on_delete: :delete_all) + add :quantity, :integer, null: false +end +``` + +**Files:** +- Migration +- Schemas +- Context + +--- + +#### #92 — Return request flow (2h) + +Customers can request returns from their account. + +**Flow:** +1. Customer goes to order detail in account +2. Clicks "Request return" (within return window) +3. Selects items and reason +4. Submits request +5. Email sent to admin + +**Files:** +- Account orders — return request button/form +- `lib/berrypod/returns.ex` — create request +- Email notification + +--- + +#### #93 — Return admin (2h) + +Admin can process return requests. + +**Admin UI:** +- `/admin/returns` — list of requests (pending, processed) +- Approve/reject with refund amount +- Status tracking + +**Actions:** +- Approve → triggers refund (Stripe or PayPal) +- Reject → email customer with reason + +**Files:** +- `lib/berrypod_web/live/admin/returns_live.ex` +- Stripe/PayPal refund integration +- Activity log entries + +--- + +#### #94 — Return policy settings (1h) + +Configure return window and reasons. + +**Settings:** +- `return_window_days` — default 30 +- `return_reasons` — list of selectable reasons + +**Files:** +- Settings +- Admin settings UI +- Return form uses configured reasons + +--- + +### Email sequences (#95–97) + +Only single campaigns exist. Need automated flows. + +#### #95 — Email sequence schema (2h) + +**Schema:** +```elixir +create table(:email_sequences, primary_key: false) do + add :id, :binary_id, primary_key: true + add :name, :string, null: false + add :trigger, :string, null: false # "welcome", "post_purchase", "browse_abandon" + add :active, :boolean, default: true + timestamps() +end + +create table(:email_sequence_steps, primary_key: false) do + add :id, :binary_id, primary_key: true + add :sequence_id, references(:email_sequences, type: :binary_id, on_delete: :delete_all) + add :position, :integer, null: false + add :delay_hours, :integer, null: false # hours after trigger/previous step + add :subject, :string, null: false + add :body, :text, null: false + timestamps() +end + +create table(:email_sequence_sends, primary_key: false) do + add :id, :binary_id, primary_key: true + add :step_id, references(:email_sequence_steps, type: :binary_id, on_delete: :delete_all) + add :customer_id, references(:customers, type: :binary_id, on_delete: :delete_all) + add :email, :string, null: false + add :sent_at, :utc_datetime + add :opened_at, :utc_datetime + add :clicked_at, :utc_datetime +end +``` + +**Files:** +- Migration +- Schemas +- Context + +--- + +#### #96 — Sequence triggers & sending (3h) + +Oban jobs to trigger and send sequence emails. + +**Triggers:** +- `welcome` — on customer registration +- `post_purchase` — on order confirmation +- `browse_abandon` — on product view without purchase (24h later) + +**Flow:** +1. Trigger event → enqueue first step job with delay +2. Job runs → send email → enqueue next step +3. Track opens/clicks (pixel + link tracking) + +**Files:** +- `lib/berrypod/workers/email_sequence_worker.ex` +- Trigger hooks in registration/order/analytics +- Email sending integration + +--- + +#### #97 — Sequence admin (2h) + +Admin can create and manage sequences. + +**Admin UI:** +- `/admin/sequences` — list of sequences +- Create/edit sequence with steps +- Preview emails +- Analytics (sent, opened, clicked) + +**Files:** +- `lib/berrypod_web/live/admin/sequences_live.ex` +- Router +- Admin nav + +--- + +### GDPR data export/deletion (#98–99) + +Right to erasure not fully implemented. + +#### #98 — Customer data export (1.5h) + +Customers can export their data. + +**Export includes:** +- Account info (email, name) +- Orders (with items, addresses) +- Reviews +- Newsletter subscription status + +**Format:** JSON download + +**Files:** +- Account settings — "Download my data" button +- `lib/berrypod/customers.ex` — export function +- Controller endpoint + +--- + +#### #99 — Customer data deletion (2h) + +Customers can delete their account. + +**Process:** +1. Customer requests deletion +2. Confirmation email sent +3. After confirmation, anonymise data: + - Remove name, email (replace with "deleted-{id}") + - Keep order records for accounting (anonymised) + - Delete reviews or anonymise + - Remove from newsletter + +**Files:** +- Account settings — "Delete my account" +- Deletion flow with confirmation +- Anonymisation logic + +--- + +--- + +## Phase 3: Scale + +### Blog functionality (#100) + +Page builder exists but no blog post type. + +#### #100 — Blog post type (3h) + +Extend page builder for blog posts. + +**New fields on pages:** +```elixir +add :page_type, :string, default: "page" # "page" or "post" +add :published_at, :utc_datetime +add :author, :string +add :category, :string +add :excerpt, :text +``` + +**Features:** +- `/blog` index page (posts sorted by date) +- `/blog/:slug` individual posts +- Category filtering +- RSS feed +- Previous/next navigation + +**Files:** +- Migration +- Page schema updates +- Blog index LiveView +- RSS controller +- Admin pages — post type option + +--- + +### Staff accounts (#101) + +Single admin only. Need team access. + +#### #101 — Staff accounts & RBAC (4h) + +Multiple staff with role-based permissions. + +**Schema:** +```elixir +alter table(:users) do + add :role, :string, default: "owner" # owner, admin, staff + add :permissions, :map, default: %{} +end +``` + +**Permissions:** +- `orders` — view/manage orders +- `products` — view/manage products +- `pages` — edit pages +- `settings` — access settings +- `analytics` — view analytics + +**Admin UI:** +- `/admin/team` — list staff +- Invite staff (email with setup link) +- Assign role/permissions +- Activity log shows who did what + +**Files:** +- Migration +- User schema update +- Permission checks in LiveViews +- Team management LiveView +- Invite flow + +--- + +## Dependencies + +| Task | Depends on | +|------|------------| +| #77 (Link orders to customers) | #75, #76 | +| #78 (Account dashboard) | #76 | +| #79 (Saved addresses) | #76 | +| #80 (Guest checkout linking) | #75, #76 | +| #85 (Review submission) | #84, #75 (optional) | +| #92 (Return request flow) | #91, #78 | +| #96 (Sequence triggers) | #95 | +| #98, #99 (GDPR) | #75 | + +--- + +## Already covered elsewhere + +These gaps are addressed in other plans: + +| Gap | Covered in | +|-----|-----------| +| Discount/coupon codes | profit-aware-pricing.md (#69) | +| Tax management | profit-aware-pricing.md (#66) | +| Profit dashboard | profit-aware-pricing.md (#67) | +| Margin warnings | profit-aware-pricing.md (#68, #70) | +| Announcement bar | profit-aware-pricing.md (#71) | +| SEO enhancements | seo-enhancements.md | +| Multiple print providers | ROADMAP.md | +| Buy-now-pay-later (Klarna) | Future (dependent on demand) |