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

18 KiB
Raw Blame 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:

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.


Associate orders with customer accounts.

Migration:

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:

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:

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:

{
  "@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:

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:

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:

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:

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)