diff --git a/ROADMAP_UPDATED.md b/ROADMAP_UPDATED.md new file mode 100644 index 0000000..1b0ef6b --- /dev/null +++ b/ROADMAP_UPDATED.md @@ -0,0 +1,503 @@ +# SimpleShop Roadmap + +This document tracks future improvements, features, and known gaps. + +--- + +## Core MVP: Real Products & Checkout (Priority) + +This section covers the work needed to turn SimpleShop from a theme demo into a working e-commerce storefront. Estimated total effort: **3-4 days** (leveraging existing Printify demo code). + +### Phase A: Products Context + Printify Sync +**Status:** Not implemented +**Effort:** 1-1.5 days +**Dependencies:** None + +Replace `PreviewData` with real products synced from Printify. + +**Schemas:** +```elixir +# products table +field :printify_id, :string +field :title, :string +field :description, :text +field :images, {:array, :map} # [{src, position}] +field :print_provider_id, :integer +field :blueprint_id, :integer +field :synced_at, :utc_datetime +field :published, :boolean, default: true +timestamps() + +# product_variants table +field :printify_variant_id, :integer +field :title, :string # e.g. "Black / M" +field :sku, :string +field :price_cents, :integer # selling price +field :cost_cents, :integer # Printify cost (for profit calc) +field :options, :map # %{"Color" => "Black", "Size" => "M"} +field :is_available, :boolean +belongs_to :product + +# product_cost_history table (append-only for future analytics) +field :cost_cents, :integer +field :recorded_at, :utc_datetime +belongs_to :product_variant +``` + +**Implementation:** +1. Migrate OAuth + Client modules from `simpleshop_printify` demo + - Adapt `Simpleshop.Printify.OAuth` → `SimpleshopTheme.Printify.OAuth` + - Adapt `Simpleshop.Printify.Client` → `SimpleshopTheme.Printify.Client` + - Adapt `Simpleshop.Printify.TokenStore` → `SimpleshopTheme.Printify.TokenStore` +2. Create `SimpleshopTheme.Products` context with schemas +3. Add `mix sync_products` task to pull products from Printify +4. Add webhook endpoint for `product:publish:started` events +5. Replace `PreviewData` calls in LiveViews with `Products` context queries +6. Store cost history on each sync for future profit analytics + +**Webhook flow:** +1. Seller clicks "Publish to SimpleShop" in Printify dashboard +2. Printify fires `product:publish:started` with product data +3. SimpleShop stores product locally in SQLite +4. SimpleShop calls Printify "publish succeeded" endpoint + +**Files to create:** +- `lib/simpleshop_theme/printify/oauth.ex` +- `lib/simpleshop_theme/printify/token_store.ex` +- `lib/simpleshop_theme/products.ex` +- `lib/simpleshop_theme/products/product.ex` +- `lib/simpleshop_theme/products/variant.ex` +- `lib/simpleshop_theme/products/cost_history.ex` +- `lib/simpleshop_theme_web/controllers/printify_webhook_controller.ex` +- `lib/mix/tasks/sync_products.ex` +- `priv/repo/migrations/*_create_products.exs` + +--- + +### Phase B: Session-Based Cart +**Status:** Not implemented +**Effort:** 0.5 days +**Dependencies:** Phase A (Products) + +Real cart functionality with session persistence. + +**Approach:** Store cart in Phoenix session (no separate cart table needed for MVP). Cart is a map of `%{variant_id => quantity}` stored in the session. + +**Implementation:** +```elixir +# lib/simpleshop_theme/cart.ex +defmodule SimpleshopTheme.Cart do + alias SimpleshopTheme.Products + + def get(session), do: Map.get(session, "cart", %{}) + + def add_item(session, variant_id, quantity \\ 1) + def remove_item(session, variant_id) + def update_quantity(session, variant_id, quantity) + def clear(session) + + def to_line_items(cart) do + # Returns list of %{variant: variant, quantity: qty, subtotal: price} + end + + def total(cart) # Returns total in cents + def item_count(cart) # For header badge +end +``` + +**LiveView integration:** +- Add `phx-click="add_to_cart"` to product pages +- Update cart LiveView to use real data +- Add cart count to header (assign in `on_mount`) + +**Files to create/modify:** +- `lib/simpleshop_theme/cart.ex` +- Modify `lib/simpleshop_theme_web/live/shop_live/product_show.ex` +- Modify `lib/simpleshop_theme_web/live/shop_live/cart.ex` +- Modify `lib/simpleshop_theme_web/components/layouts.ex` (cart count) + +--- + +### Phase C: Stripe Checkout +**Status:** Not implemented +**Effort:** 0.5-1 day +**Dependencies:** Phase B (Cart) + +Stripe Checkout (hosted payment page) integration. + +**Dependencies to add:** +```elixir +# mix.exs +{:stripity_stripe, "~> 3.0"} +``` + +**Config:** +```elixir +# config/runtime.exs +config :stripity_stripe, + api_key: System.get_env("STRIPE_SECRET_KEY") + +# Also need STRIPE_WEBHOOK_SECRET for webhook verification +``` + +**Implementation:** +```elixir +# lib/simpleshop_theme/checkout.ex +defmodule SimpleshopTheme.Checkout do + def create_session(cart, success_url, cancel_url) do + line_items = Enum.map(cart, fn {variant_id, qty} -> + variant = Products.get_variant!(variant_id) + %{ + price_data: %{ + currency: "gbp", + unit_amount: variant.price_cents, + product_data: %{ + name: "#{variant.product.title} - #{variant.title}", + images: [hd(variant.product.images)["src"]] + } + }, + quantity: qty + } + end) + + Stripe.Checkout.Session.create(%{ + mode: "payment", + line_items: line_items, + success_url: success_url <> "?session_id={CHECKOUT_SESSION_ID}", + cancel_url: cancel_url, + shipping_address_collection: %{allowed_countries: ["GB"]}, + metadata: %{cart: Jason.encode!(cart)} + }) + end +end +``` + +**Webhook handler:** +```elixir +# lib/simpleshop_theme_web/controllers/stripe_webhook_controller.ex +def handle_event(%Stripe.Event{type: "checkout.session.completed"} = event) do + session = event.data.object + cart = Jason.decode!(session.metadata["cart"]) + shipping = session.shipping_details + + # Create order and push to Printify (Phase D) + Orders.create_from_checkout(session, cart, shipping) +end +``` + +**Files to create:** +- `lib/simpleshop_theme/checkout.ex` +- `lib/simpleshop_theme_web/controllers/stripe_webhook_controller.ex` +- `lib/simpleshop_theme_web/live/shop_live/checkout_success.ex` +- `lib/simpleshop_theme_web/live/shop_live/checkout_cancel.ex` + +**Routes:** +```elixir +post "/webhooks/stripe", StripeWebhookController, :handle +live "/checkout/success", ShopLive.CheckoutSuccess +live "/checkout/cancel", ShopLive.CheckoutCancel +``` + +--- + +### Phase D: Orders + Printify Fulfillment +**Status:** Not implemented +**Effort:** 0.5-1 day +**Dependencies:** Phase C (Stripe Checkout) + +Create orders locally and push to Printify for fulfillment. + +**Schema:** +```elixir +# orders table +field :stripe_session_id, :string +field :stripe_payment_intent_id, :string +field :printify_order_id, :string +field :status, Ecto.Enum, values: [:pending, :paid, :submitted, :in_production, :shipped, :delivered, :cancelled] +field :total_cents, :integer +field :shipping_address, :map +field :customer_email, :string +timestamps() + +# order_items table +field :quantity, :integer +field :unit_price_cents, :integer +field :unit_cost_cents, :integer # For profit tracking +belongs_to :order +belongs_to :product_variant +``` + +**Implementation:** +```elixir +# lib/simpleshop_theme/orders.ex +def create_from_checkout(stripe_session, cart, shipping) do + Repo.transaction(fn -> + # 1. Create local order + order = create_order(stripe_session, shipping) + + # 2. Create order items + create_order_items(order, cart) + + # 3. Push to Printify + case Printify.Client.create_order(order) do + {:ok, printify_response} -> + update_order(order, %{ + printify_order_id: printify_response["id"], + status: :submitted + }) + {:error, reason} -> + # Log error but don't fail - can retry later + Logger.error("Failed to submit to Printify: #{inspect(reason)}") + update_order(order, %{status: :paid}) # Mark as paid, needs manual submission + end + end) +end +``` + +**Printify order creation:** +```elixir +# Add to lib/simpleshop_theme/printify/client.ex +def create_order(order) do + body = %{ + external_id: order.id, + shipping_method: 1, # Standard + address_to: %{ + first_name: order.shipping_address["name"] |> String.split() |> hd(), + last_name: order.shipping_address["name"] |> String.split() |> List.last(), + email: order.customer_email, + address1: order.shipping_address["line1"], + address2: order.shipping_address["line2"], + city: order.shipping_address["city"], + zip: order.shipping_address["postal_code"], + country: order.shipping_address["country"] + }, + line_items: Enum.map(order.items, fn item -> + %{ + product_id: item.variant.product.printify_id, + variant_id: item.variant.printify_variant_id, + quantity: item.quantity + } + end) + } + + post("/shops/#{shop_id()}/orders.json", body) +end +``` + +**Files to create:** +- `lib/simpleshop_theme/orders.ex` +- `lib/simpleshop_theme/orders/order.ex` +- `lib/simpleshop_theme/orders/order_item.ex` +- `priv/repo/migrations/*_create_orders.exs` +- Update `lib/simpleshop_theme/printify/client.ex` with `create_order/1` + +--- + +### Phase E: Cost Verification at Checkout (Safety Net) +**Status:** Not implemented +**Effort:** 0.25 days +**Dependencies:** Phase D (Orders) + +Verify Printify costs haven't changed before completing checkout. + +**Implementation:** +```elixir +# In checkout flow, before creating Stripe session +def verify_costs(cart) do + Enum.reduce_while(cart, :ok, fn {variant_id, _qty}, _acc -> + variant = Products.get_variant!(variant_id) + + case Printify.Client.get_product(variant.product.printify_id) do + {:ok, printify_product} -> + current_cost = find_variant_cost(printify_product, variant.printify_variant_id) + + if current_cost != variant.cost_cents do + # Update local cost + Products.update_variant_cost(variant, current_cost) + + if cost_increase_exceeds_threshold?(variant.cost_cents, current_cost) do + {:halt, {:error, :costs_changed, variant}} + else + {:cont, :ok} + end + else + {:cont, :ok} + end + + {:error, _} -> + # Can't verify - proceed but log warning + Logger.warning("Could not verify costs for variant #{variant_id}") + {:cont, :ok} + end + end) +end +``` + +This ensures sellers never unknowingly sell at a loss due to Printify price changes. + +--- + +## Quick Wins (Low Effort) + +### CSS Cache Warming on Startup +**Status:** Not implemented +**Effort:** Small + +Currently the CSS cache (ETS) is created on startup but not pre-warmed. The first request after server restart generates CSS on-demand. + +**Implementation:** +Add cache warming to `lib/simpleshop_theme/application.ex`: +```elixir +# After supervisor starts +Task.start(fn -> + settings = SimpleshopTheme.Settings.get_theme_settings() + css = SimpleshopTheme.Theme.CSSGenerator.generate(settings) + SimpleshopTheme.Theme.CSSCache.put(css) +end) +``` + +### Navigation Links Between Admin and Shop +**Status:** Not implemented +**Effort:** Small + +No links exist to navigate between the theme editor (`/admin/theme`) and the public shop (`/`). + +**Implementation:** +- Add "View Shop" button in theme editor header +- Add "Edit Theme" link in shop header (when authenticated) + +### Collection Slug Routes +**Status:** Partial +**Effort:** Small + +Currently we have `/products` but the original plan included `/collections/:slug` for filtered views by category. + +### Enhanced Contact Page +**Status:** Not implemented +**Effort:** Small + +The current contact page has subtle footer social icons that are easy to miss. Improvements: + +1. **Newsletter signup card** - Prominent card encouraging newsletter subscription as the best way to stay updated (already in footer, but deserves dedicated placement) + +2. **Social media links card** - Full-width card listing social platforms with icons and text labels: + ``` + [Instagram icon] Instagram + [Patreon icon] Patreon + [TikTok icon] TikTok + [Facebook icon] Facebook + [Pinterest icon] Pinterest + ``` + This makes social links more discoverable than the current small footer icons. + +**Implementation:** +- Add `newsletter_card/1` component to ShopComponents +- Add `social_links_card/1` component with configurable platforms +- Update contact page template to include both cards +- Consider making platforms configurable via theme settings + +--- + +## Medium Features + +### Page Builder (Database-Driven Pages) +**Status:** Planned (see `docs/plans/page-builder.md`) +**Effort:** Large + +Allow shop owners to build custom pages by combining pre-built sections: +- Hero, Featured Products, Testimonials, Newsletter, etc. +- Drag-and-drop section ordering +- Per-section configuration +- Database-backed page storage + +--- + +## Future Features (Large Scope) + +### Multi-Admin Support +Currently single-user authentication: +- Multiple admin users +- Role-based permissions +- Audit logging + +### Custom Domains +Allow shops to use their own domain: +- Domain verification +- SSL certificate provisioning +- DNS configuration guidance + +### Theme Export/Import +Backup and restore theme settings: +- JSON export of all settings +- Import with validation +- Preset sharing between shops + +### Advanced Theme Features +- Custom CSS injection +- Custom JavaScript snippets +- Code-level overrides for developers + +### Multi-Provider Support (Future) +Support multiple POD providers beyond Printify: +- Prodigi (better for art prints) +- Gelato (global fulfillment) +- Provider-agnostic product model +- Price comparison across providers + +--- + +## Technical Debt + +### Test Coverage +Phase 9 testing is basic. Areas needing better coverage: +- Shop LiveView integration tests +- CSS cache invalidation flow +- Theme application across all pages +- Responsive behaviour +- Accessibility validation + +### Error Handling +- Better error states for missing products +- Graceful degradation when theme settings are invalid +- Network error handling in LiveView + +### Rename Project to SimpleShop +**Status:** Not implemented +**Effort:** Medium + +The project is currently named `simpleshop_theme` (reflecting its origins as a theme system), but it's now a full e-commerce storefront. Rename to `simple_shop` or `simpleshop` to reflect this. + +**Files to update:** +- `mix.exs` - app name +- `lib/simpleshop_theme/` → `lib/simple_shop/` +- `lib/simpleshop_theme_web/` → `lib/simple_shop_web/` +- All module names (`SimpleshopTheme` → `SimpleShop`) +- `config/*.exs` - endpoint and repo references +- `test/` directories +- Database file name + +--- + +## Completed (For Reference) + +### Sample Content ("Wildprint Studio") ✅ +- 16 POD products across 5 categories +- Nature/botanical theme with testimonials +- UK-focused (prices in £) +- Printify API integration for mockup generation (`mix generate_mockups`) + +### Phase 1-8: Theme Editor ✅ +- Theme settings schema and persistence +- CSS three-layer architecture +- 8 theme presets +- All customisation controls +- Logo/header image uploads +- SVG recolouring +- Preview system with 7 pages + +### Phase 9: Storefront Integration ✅ +- Public shop routes (/, /products, /products/:id, /cart, /about, /contact) +- Shared PageTemplates for shop and preview +- CSS injection via shop layout +- Themed error pages (404/500) +- Dev routes for error page preview