berrypod/docs/plans/competitive-gaps.md

706 lines
18 KiB
Markdown
Raw Normal View History

# 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) |