berrypod/docs/plans/competitive-gaps.md
jamey 255912af73 update docs and progress tracking
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-03-13 13:34:36 +00:00

706 lines
18 KiB
Markdown
Raw 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.

# Competitive gaps
> Status: Planned
> Tasks: #75101 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 (#7580)
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 (#8183)
~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 (#8488)
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 (#8990) — 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 (#9194)
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 (#9597)
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 (#9899)
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) |