update docs and progress tracking
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
f2e9960303
commit
255912af73
11
.envrc.example
Normal file
11
.envrc.example
Normal file
@ -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"
|
||||||
47
PROGRESS.md
47
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 |
|
| 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))
|
### 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.
|
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 |
|
| [editor-reorganisation.md](docs/plans/editor-reorganisation.md) | Planned |
|
||||||
| [seo-enhancements.md](docs/plans/seo-enhancements.md) | Planned |
|
| [seo-enhancements.md](docs/plans/seo-enhancements.md) | Planned |
|
||||||
| [competitive-gap-analysis.md](docs/plans/competitive-gap-analysis.md) | Reference |
|
| [competitive-gap-analysis.md](docs/plans/competitive-gap-analysis.md) | Reference |
|
||||||
|
| [competitive-gaps.md](docs/plans/competitive-gaps.md) | Planned |
|
||||||
|
|||||||
54
README.md
54
README.md
@ -30,6 +30,7 @@ Complete storefront with all the pages you need:
|
|||||||
### Technical highlights
|
### Technical highlights
|
||||||
- Hand-written CSS with three-layer architecture (9.8 KB gzipped shop, 17.8 KB admin)
|
- Hand-written CSS with three-layer architecture (9.8 KB gzipped shop, 17.8 KB admin)
|
||||||
- SQLite with BLOB storage, IMMEDIATE transactions, WAL, mmap
|
- 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)
|
- Image optimisation pipeline (AVIF/WebP/JPEG responsive variants via Oban)
|
||||||
- ETS caching for CSS, pages, redirects, favicons
|
- ETS caching for CSS, pages, redirects, favicons
|
||||||
- 99-100 PageSpeed mobile, no-JS support across all key flows
|
- 99-100 PageSpeed mobile, no-JS support across all key flows
|
||||||
@ -102,6 +103,59 @@ assets/css/
|
|||||||
└── theme-layer3-semantic.css # component styles
|
└── 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
|
## Stripe setup
|
||||||
|
|
||||||
1. Create a [Stripe account](https://dashboard.stripe.com/register)
|
1. Create a [Stripe account](https://dashboard.stripe.com/register)
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
# Competitive Gap Analysis
|
# Competitive Gap Analysis
|
||||||
|
|
||||||
Status: Reference
|
Status: Reference (implementation plan: [competitive-gaps.md](competitive-gaps.md))
|
||||||
|
|
||||||
## Overview
|
## Overview
|
||||||
|
|
||||||
|
|||||||
705
docs/plans/competitive-gaps.md
Normal file
705
docs/plans/competitive-gaps.md
Normal file
@ -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) |
|
||||||
Loading…
Reference in New Issue
Block a user