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