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