706 lines
18 KiB
Markdown
706 lines
18 KiB
Markdown
# 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) |
|