rename project from SimpleshopTheme to Berrypod
All modules, configs, paths, and references updated. 836 tests pass, zero warnings. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -113,7 +113,7 @@ Build the structural pieces first, using existing DaisyUI components. No CSS cha
|
||||
|
||||
### 1.1 Admin shell layout
|
||||
|
||||
**New file:** `lib/simpleshop_theme_web/components/admin_components.ex`
|
||||
**New file:** `lib/berrypod_web/components/admin_components.ex`
|
||||
|
||||
A shared admin layout component that wraps all admin pages:
|
||||
|
||||
@@ -156,7 +156,7 @@ The shell provides:
|
||||
|
||||
### 1.2 Admin root layout
|
||||
|
||||
**File:** `lib/simpleshop_theme_web/components/layouts/admin_root.html.heex` (new)
|
||||
**File:** `lib/berrypod_web/components/layouts/admin_root.html.heex` (new)
|
||||
|
||||
A dedicated root layout for admin pages that:
|
||||
- Loads `app.css` (or later `app-admin.css`)
|
||||
@@ -164,7 +164,7 @@ A dedicated root layout for admin pages that:
|
||||
- Doesn't include the shop nav/footer chrome
|
||||
- Replaces the current generic `root.html.heex` for admin routes
|
||||
|
||||
**File:** `lib/simpleshop_theme_web/components/layouts/admin.html.heex` (new)
|
||||
**File:** `lib/berrypod_web/components/layouts/admin.html.heex` (new)
|
||||
|
||||
The admin child layout (equivalent of `shop.html.heex`):
|
||||
- Renders flash messages
|
||||
@@ -176,7 +176,7 @@ The admin child layout (equivalent of `shop.html.heex`):
|
||||
|
||||
### 1.3 Dashboard page
|
||||
|
||||
**File:** `lib/simpleshop_theme_web/live/admin_live/dashboard.ex` (new)
|
||||
**File:** `lib/berrypod_web/live/admin_live/dashboard.ex` (new)
|
||||
|
||||
Replace the current `/admin` → redirect-to-theme with a proper dashboard:
|
||||
|
||||
@@ -195,7 +195,7 @@ When the shop isn't live yet, the dashboard IS the setup wizard (from [setup-wiz
|
||||
|
||||
Currently settings are split across three pages. Consolidate into one settings page with sections:
|
||||
|
||||
**File:** `lib/simpleshop_theme_web/live/admin_live/settings.ex` (refactor)
|
||||
**File:** `lib/berrypod_web/live/admin_live/settings.ex` (refactor)
|
||||
|
||||
Sections:
|
||||
- **Payments** — Stripe API key, webhook config (current `/admin/settings` content)
|
||||
@@ -209,7 +209,7 @@ Each section is a collapsible card or tab. The existing `/admin/providers` and `
|
||||
|
||||
### 1.5 Admin bar on shop pages
|
||||
|
||||
**File:** `lib/simpleshop_theme_web/components/shop_components/layout.ex`
|
||||
**File:** `lib/berrypod_web/components/shop_components/layout.ex`
|
||||
|
||||
When the admin is browsing the shop (authenticated), show a thin bar at the top:
|
||||
|
||||
@@ -313,12 +313,12 @@ Replaces DaisyUI for admin pages. Structure:
|
||||
@import "tailwindcss" source(none);
|
||||
@source "../css";
|
||||
@source "../js";
|
||||
@source "../../lib/simpleshop_theme_web/live/admin";
|
||||
@source "../../lib/simpleshop_theme_web/live/user";
|
||||
@source "../../lib/simpleshop_theme_web/components/admin_components.ex";
|
||||
@source "../../lib/simpleshop_theme_web/components/core_components.ex";
|
||||
@source "../../lib/simpleshop_theme_web/components/layouts/admin_root.html.heex";
|
||||
@source "../../lib/simpleshop_theme_web/components/layouts/admin.html.heex";
|
||||
@source "../../lib/berrypod_web/live/admin";
|
||||
@source "../../lib/berrypod_web/live/user";
|
||||
@source "../../lib/berrypod_web/components/admin_components.ex";
|
||||
@source "../../lib/berrypod_web/components/core_components.ex";
|
||||
@source "../../lib/berrypod_web/components/layouts/admin_root.html.heex";
|
||||
@source "../../lib/berrypod_web/components/layouts/admin.html.heex";
|
||||
|
||||
@plugin "../vendor/heroicons";
|
||||
|
||||
@@ -353,7 +353,7 @@ Each is ~5-15 lines of CSS. No need for DaisyUI's full component library.
|
||||
|
||||
### 2.3 Migrate core_components.ex
|
||||
|
||||
**File:** `lib/simpleshop_theme_web/components/core_components.ex`
|
||||
**File:** `lib/berrypod_web/components/core_components.ex`
|
||||
|
||||
Replace DaisyUI class references with the new admin classes. This is a find-and-replace job:
|
||||
- `btn btn-primary` → `admin-btn admin-btn-primary`
|
||||
|
||||
@@ -202,7 +202,7 @@ Acceptance: layout primitives available, no visual changes, all tests pass.
|
||||
- Visual regression: PDP page
|
||||
|
||||
Files modified:
|
||||
- `lib/simpleshop_theme_web/components/shop_components/product.ex` (83 style= -> 0)
|
||||
- `lib/berrypod_web/components/shop_components/product.ex` (83 style= -> 0)
|
||||
- `assets/css/shop/components.css`
|
||||
|
||||
Acceptance: `product.ex` has zero inline styles, visual regression clean.
|
||||
@@ -224,8 +224,8 @@ Acceptance: `product.ex` has zero inline styles, visual regression clean.
|
||||
- Visual regression: cart page, cart drawer
|
||||
|
||||
Files modified:
|
||||
- `lib/simpleshop_theme_web/components/shop_components/layout.ex` (59 -> 0)
|
||||
- `lib/simpleshop_theme_web/components/shop_components/cart.ex` (51 -> 0)
|
||||
- `lib/berrypod_web/components/shop_components/layout.ex` (59 -> 0)
|
||||
- `lib/berrypod_web/components/shop_components/cart.ex` (51 -> 0)
|
||||
- `assets/css/shop/components.css`
|
||||
|
||||
Acceptance: both files zero inline styles, visual regression clean.
|
||||
@@ -246,9 +246,9 @@ Acceptance: both files zero inline styles, visual regression clean.
|
||||
- Also `base.ex` (2)
|
||||
|
||||
Files modified:
|
||||
- `lib/simpleshop_theme_web/components/shop_components/content.ex` (57 -> 0)
|
||||
- `lib/simpleshop_theme_web/components/shop_components/base.ex` (2 -> 0)
|
||||
- `lib/simpleshop_theme_web/components/page_templates/*.html.heex` (29 -> 0)
|
||||
- `lib/berrypod_web/components/shop_components/content.ex` (57 -> 0)
|
||||
- `lib/berrypod_web/components/shop_components/base.ex` (2 -> 0)
|
||||
- `lib/berrypod_web/components/page_templates/*.html.heex` (29 -> 0)
|
||||
- `assets/css/shop/components.css`
|
||||
|
||||
Acceptance: **zero inline styles remain** (0/281), full visual regression clean.
|
||||
@@ -268,7 +268,7 @@ Acceptance: **zero inline styles remain** (0/281), full visual regression clean.
|
||||
**5b** — Remove Tailwind shop build (~1.5h):
|
||||
- Replace remaining Tailwind classes in `.heex` page templates
|
||||
- Remove `@import "tailwindcss"` from `app-shop.css`
|
||||
- Remove `simpleshop_theme_shop` Tailwind profile from `config/config.exs`
|
||||
- Remove `berrypod_shop` Tailwind profile from `config/config.exs`
|
||||
- Remove `tailwind_shop` watcher from `config/dev.exs`
|
||||
- Update `assets.build` and `assets.deploy` Mix aliases
|
||||
- Full visual regression
|
||||
@@ -301,7 +301,7 @@ Acceptance: no Tailwind classes in shop code, Tailwind shop build removed, admin
|
||||
Files modified:
|
||||
- `assets/css/admin-components.css` (new)
|
||||
- `assets/css/app.css` (remove DaisyUI)
|
||||
- `lib/simpleshop_theme_web/components/core_components.ex`
|
||||
- `lib/berrypod_web/components/core_components.ex`
|
||||
- All admin LiveView files
|
||||
- Auth LiveView files
|
||||
|
||||
|
||||
@@ -133,7 +133,7 @@ end
|
||||
|
||||
**Net saving:** ~65 lines removed (80 duplicated minus ~15 for the helper function).
|
||||
|
||||
**Files:** `lib/simpleshop_theme_web/live/theme_live/index.ex`
|
||||
**Files:** `lib/berrypod_web/live/theme_live/index.ex`
|
||||
|
||||
**Complexity:** Low. Pure refactor within one file.
|
||||
|
||||
|
||||
@@ -126,9 +126,9 @@ For a production-quality system, Oban's durability is worth the small complexity
|
||||
|
||||
```elixir
|
||||
# config/config.exs
|
||||
config :simpleshop_theme, Oban,
|
||||
config :berrypod, Oban,
|
||||
engine: Oban.Engines.Lite, # SQLite support
|
||||
repo: SimpleshopTheme.Repo,
|
||||
repo: Berrypod.Repo,
|
||||
plugins: [
|
||||
# Prune completed jobs after 60 seconds - keeps DB lean
|
||||
{Oban.Plugins.Pruner, max_age: 60}
|
||||
@@ -144,7 +144,7 @@ With `max_age: 60`, the Oban tables typically contain:
|
||||
### Job Worker
|
||||
|
||||
```elixir
|
||||
defmodule SimpleshopTheme.Workers.ImageVariants do
|
||||
defmodule Berrypod.Workers.ImageVariants do
|
||||
use Oban.Worker, queue: :images, max_attempts: 3
|
||||
|
||||
@impl Oban.Worker
|
||||
@@ -168,7 +168,7 @@ def upload_image(attrs) do
|
||||
{:ok, image} ->
|
||||
# Enqueue async variant generation
|
||||
%{image_id: image.id}
|
||||
|> SimpleshopTheme.Workers.ImageVariants.new()
|
||||
|> Berrypod.Workers.ImageVariants.new()
|
||||
|> Oban.insert()
|
||||
|
||||
{:ok, image}
|
||||
@@ -195,7 +195,7 @@ defp ensure_all_variants do
|
||||
|> Enum.each(fn image ->
|
||||
# Re-enqueue for processing
|
||||
%{image_id: image.id}
|
||||
|> SimpleshopTheme.Workers.ImageVariants.new()
|
||||
|> Berrypod.Workers.ImageVariants.new()
|
||||
|> Oban.insert()
|
||||
end)
|
||||
end
|
||||
@@ -223,7 +223,7 @@ end
|
||||
**File:** `priv/repo/migrations/YYYYMMDDHHMMSS_add_image_metadata.exs`
|
||||
|
||||
```elixir
|
||||
defmodule SimpleshopTheme.Repo.Migrations.AddImageMetadata do
|
||||
defmodule Berrypod.Repo.Migrations.AddImageMetadata do
|
||||
use Ecto.Migration
|
||||
|
||||
def change do
|
||||
@@ -244,10 +244,10 @@ end
|
||||
|
||||
### Step 3: Image Optimizer Module
|
||||
|
||||
**File:** `lib/simpleshop_theme/images/optimizer.ex`
|
||||
**File:** `lib/berrypod/images/optimizer.ex`
|
||||
|
||||
```elixir
|
||||
defmodule SimpleshopTheme.Images.Optimizer do
|
||||
defmodule Berrypod.Images.Optimizer do
|
||||
@moduledoc """
|
||||
Generates optimized image variants. Only creates sizes ≤ source dimensions.
|
||||
"""
|
||||
@@ -290,7 +290,7 @@ defmodule SimpleshopTheme.Images.Optimizer do
|
||||
Called by Oban worker.
|
||||
"""
|
||||
def process_for_image(image_id) do
|
||||
alias SimpleshopTheme.{Repo, Media.Image}
|
||||
alias Berrypod.{Repo, Media.Image}
|
||||
|
||||
case Repo.get(Image, image_id) do
|
||||
nil ->
|
||||
@@ -387,16 +387,16 @@ end
|
||||
|
||||
### Step 4: Oban Worker
|
||||
|
||||
**File:** `lib/simpleshop_theme/workers/image_variants.ex`
|
||||
**File:** `lib/berrypod/workers/image_variants.ex`
|
||||
|
||||
```elixir
|
||||
defmodule SimpleshopTheme.Workers.ImageVariants do
|
||||
defmodule Berrypod.Workers.ImageVariants do
|
||||
use Oban.Worker,
|
||||
queue: :images,
|
||||
max_attempts: 3,
|
||||
unique: [period: 60] # Prevent duplicate jobs
|
||||
|
||||
alias SimpleshopTheme.Images.Optimizer
|
||||
alias Berrypod.Images.Optimizer
|
||||
|
||||
@impl Oban.Worker
|
||||
def perform(%Oban.Job{args: %{"image_id" => image_id}}) do
|
||||
@@ -410,11 +410,11 @@ end
|
||||
|
||||
### Step 5: Update Media Module
|
||||
|
||||
**File:** `lib/simpleshop_theme/media.ex`
|
||||
**File:** `lib/berrypod/media.ex`
|
||||
|
||||
```elixir
|
||||
alias SimpleshopTheme.Images.Optimizer
|
||||
alias SimpleshopTheme.Workers.ImageVariants
|
||||
alias Berrypod.Images.Optimizer
|
||||
alias Berrypod.Workers.ImageVariants
|
||||
|
||||
def upload_image(attrs) do
|
||||
# Convert to lossless WebP before storing
|
||||
@@ -449,10 +449,10 @@ end
|
||||
|
||||
### Step 6: Startup Recovery GenServer
|
||||
|
||||
**File:** `lib/simpleshop_theme/images/variant_cache.ex`
|
||||
**File:** `lib/berrypod/images/variant_cache.ex`
|
||||
|
||||
```elixir
|
||||
defmodule SimpleshopTheme.Images.VariantCache do
|
||||
defmodule Berrypod.Images.VariantCache do
|
||||
@moduledoc """
|
||||
Ensures all image variants exist on startup.
|
||||
Enqueues Oban jobs for any missing variants.
|
||||
@@ -461,10 +461,10 @@ defmodule SimpleshopTheme.Images.VariantCache do
|
||||
use GenServer
|
||||
require Logger
|
||||
|
||||
alias SimpleshopTheme.Repo
|
||||
alias SimpleshopTheme.Media.Image, as: ImageSchema
|
||||
alias SimpleshopTheme.Images.Optimizer
|
||||
alias SimpleshopTheme.Workers.ImageVariants
|
||||
alias Berrypod.Repo
|
||||
alias Berrypod.Media.Image, as: ImageSchema
|
||||
alias Berrypod.Images.Optimizer
|
||||
alias Berrypod.Workers.ImageVariants
|
||||
import Ecto.Query
|
||||
|
||||
def start_link(_opts) do
|
||||
@@ -521,7 +521,7 @@ end
|
||||
|
||||
### Step 7: Responsive Image Component
|
||||
|
||||
**File:** `lib/simpleshop_theme_web/components/shop_components.ex`
|
||||
**File:** `lib/berrypod_web/components/shop_components.ex`
|
||||
|
||||
```elixir
|
||||
@doc """
|
||||
@@ -547,7 +547,7 @@ attr :height, :integer, default: nil
|
||||
attr :priority, :boolean, default: false
|
||||
|
||||
def responsive_image(assigns) do
|
||||
alias SimpleshopTheme.Images.Optimizer
|
||||
alias Berrypod.Images.Optimizer
|
||||
|
||||
# Compute available widths from source dimensions
|
||||
available = Optimizer.applicable_widths(assigns.source_width)
|
||||
@@ -596,7 +596,7 @@ end
|
||||
|
||||
### Step 8: Update Thumbnail Serving
|
||||
|
||||
**File:** `lib/simpleshop_theme_web/controllers/image_controller.ex`
|
||||
**File:** `lib/berrypod_web/controllers/image_controller.ex`
|
||||
|
||||
```elixir
|
||||
def thumbnail(conn, %{"id" => id}) do
|
||||
@@ -750,20 +750,20 @@ end
|
||||
## File Changes Summary
|
||||
|
||||
### Create:
|
||||
- `lib/simpleshop_theme/images/optimizer.ex` - Core optimization logic
|
||||
- `lib/simpleshop_theme/images/variant_cache.ex` - Startup recovery GenServer
|
||||
- `lib/simpleshop_theme/workers/image_variants.ex` - Oban worker
|
||||
- `lib/berrypod/images/optimizer.ex` - Core optimization logic
|
||||
- `lib/berrypod/images/variant_cache.ex` - Startup recovery GenServer
|
||||
- `lib/berrypod/workers/image_variants.ex` - Oban worker
|
||||
- `lib/mix/tasks/optimize_images.ex` - Mockup batch processing
|
||||
- `priv/repo/migrations/*_add_image_metadata.exs` - Schema + Oban tables
|
||||
|
||||
### Modify:
|
||||
- `mix.exs` - Add Oban dependency
|
||||
- `config/config.exs` - Oban configuration
|
||||
- `lib/simpleshop_theme/media/image.ex` - Add source_width/height, remove thumbnail_data
|
||||
- `lib/simpleshop_theme/media.ex` - Convert to WebP, enqueue Oban job
|
||||
- `lib/simpleshop_theme/application.ex` - Add Oban + VariantCache to supervision
|
||||
- `lib/simpleshop_theme_web/components/shop_components.ex` - Responsive image component
|
||||
- `lib/simpleshop_theme_web/controllers/image_controller.ex` - Serve thumbnails from disk
|
||||
- `lib/berrypod/media/image.ex` - Add source_width/height, remove thumbnail_data
|
||||
- `lib/berrypod/media.ex` - Convert to WebP, enqueue Oban job
|
||||
- `lib/berrypod/application.ex` - Add Oban + VariantCache to supervision
|
||||
- `lib/berrypod_web/components/shop_components.ex` - Responsive image component
|
||||
- `lib/berrypod_web/controllers/image_controller.ex` - Serve thumbnails from disk
|
||||
- `.gitignore` - Add `/priv/static/image_cache/`
|
||||
|
||||
---
|
||||
@@ -832,14 +832,14 @@ mix phx.server
|
||||
|
||||
**Files to create/modify:**
|
||||
- `priv/repo/migrations/*_add_image_metadata.exs` - New migration
|
||||
- `lib/simpleshop_theme/media/image.ex` - Add source_width/height, variants_status; remove thumbnail_data
|
||||
- `lib/berrypod/media/image.ex` - Add source_width/height, variants_status; remove thumbnail_data
|
||||
|
||||
**Tests:** `mix ecto.migrate && mix test test/simpleshop_theme/media_test.exs` (if exists)
|
||||
**Tests:** `mix ecto.migrate && mix test test/berrypod/media_test.exs` (if exists)
|
||||
|
||||
**Manual verification:**
|
||||
```bash
|
||||
# Check migration applied
|
||||
sqlite3 simpleshop_theme_dev.db ".schema images"
|
||||
sqlite3 berrypod_dev.db ".schema images"
|
||||
# Should show new columns: source_width, source_height, variants_status
|
||||
# Should NOT have: thumbnail_data
|
||||
```
|
||||
@@ -851,17 +851,17 @@ sqlite3 simpleshop_theme_dev.db ".schema images"
|
||||
### Phase 3: Optimizer Module
|
||||
|
||||
**Files to create:**
|
||||
- `lib/simpleshop_theme/images/optimizer.ex`
|
||||
- `test/simpleshop_theme/images/optimizer_test.exs`
|
||||
- `lib/berrypod/images/optimizer.ex`
|
||||
- `test/berrypod/images/optimizer_test.exs`
|
||||
- `test/support/fixtures/image_fixtures.ex`
|
||||
- `test/fixtures/sample_1200x800.jpg` (test image)
|
||||
|
||||
**Tests:** `mix test test/simpleshop_theme/images/optimizer_test.exs`
|
||||
**Tests:** `mix test test/berrypod/images/optimizer_test.exs`
|
||||
|
||||
**Manual verification:**
|
||||
```elixir
|
||||
# In iex -S mix
|
||||
alias SimpleshopTheme.Images.Optimizer
|
||||
alias Berrypod.Images.Optimizer
|
||||
Optimizer.applicable_widths(1500) # Should return [400, 800, 1200]
|
||||
Optimizer.applicable_widths(300) # Should return [300]
|
||||
```
|
||||
@@ -873,13 +873,13 @@ Optimizer.applicable_widths(300) # Should return [300]
|
||||
### Phase 4: Oban Worker
|
||||
|
||||
**Files to create:**
|
||||
- `lib/simpleshop_theme/workers/image_variants.ex`
|
||||
- `test/simpleshop_theme/workers/image_variants_test.exs`
|
||||
- `lib/berrypod/workers/image_variants.ex`
|
||||
- `test/berrypod/workers/image_variants_test.exs`
|
||||
|
||||
**Files to modify:**
|
||||
- `lib/simpleshop_theme/application.ex` - Add Oban to supervision tree
|
||||
- `lib/berrypod/application.ex` - Add Oban to supervision tree
|
||||
|
||||
**Tests:** `mix test test/simpleshop_theme/workers/image_variants_test.exs`
|
||||
**Tests:** `mix test test/berrypod/workers/image_variants_test.exs`
|
||||
|
||||
**Manual verification:**
|
||||
```bash
|
||||
@@ -894,15 +894,15 @@ mix phx.server
|
||||
### Phase 5: Media Module Integration
|
||||
|
||||
**Files to modify:**
|
||||
- `lib/simpleshop_theme/media.ex` - Update upload_image to use optimizer + enqueue worker
|
||||
- `lib/berrypod/media.ex` - Update upload_image to use optimizer + enqueue worker
|
||||
|
||||
**Tests:** `mix test test/simpleshop_theme/media_test.exs` (existing tests should pass)
|
||||
**Tests:** `mix test test/berrypod/media_test.exs` (existing tests should pass)
|
||||
|
||||
**Manual verification:**
|
||||
1. Go to `/admin/theme`
|
||||
2. Upload a new logo image
|
||||
3. Check `priv/static/image_cache/` for generated variants
|
||||
4. Check database: `sqlite3 simpleshop_theme_dev.db "SELECT id, source_width, variants_status FROM images ORDER BY inserted_at DESC LIMIT 1"`
|
||||
4. Check database: `sqlite3 berrypod_dev.db "SELECT id, source_width, variants_status FROM images ORDER BY inserted_at DESC LIMIT 1"`
|
||||
|
||||
**Commit:** `feat: integrate optimizer with image uploads`
|
||||
|
||||
@@ -911,10 +911,10 @@ mix phx.server
|
||||
### Phase 6: VariantCache GenServer
|
||||
|
||||
**Files to create:**
|
||||
- `lib/simpleshop_theme/images/variant_cache.ex`
|
||||
- `lib/berrypod/images/variant_cache.ex`
|
||||
|
||||
**Files to modify:**
|
||||
- `lib/simpleshop_theme/application.ex` - Add VariantCache to supervision tree
|
||||
- `lib/berrypod/application.ex` - Add VariantCache to supervision tree
|
||||
|
||||
**Tests:** Manual (startup behavior)
|
||||
|
||||
@@ -934,12 +934,12 @@ mix phx.server
|
||||
### Phase 7: Responsive Image Component
|
||||
|
||||
**Files to create:**
|
||||
- `test/simpleshop_theme_web/components/shop_components_test.exs`
|
||||
- `test/berrypod_web/components/shop_components_test.exs`
|
||||
|
||||
**Files to modify:**
|
||||
- `lib/simpleshop_theme_web/components/shop_components.ex` - Add responsive_image component
|
||||
- `lib/berrypod_web/components/shop_components.ex` - Add responsive_image component
|
||||
|
||||
**Tests:** `mix test test/simpleshop_theme_web/components/shop_components_test.exs`
|
||||
**Tests:** `mix test test/berrypod_web/components/shop_components_test.exs`
|
||||
|
||||
**Manual verification:**
|
||||
```elixir
|
||||
@@ -954,7 +954,7 @@ mix phx.server
|
||||
### Phase 8: ImageController Disk Serving
|
||||
|
||||
**Files to modify:**
|
||||
- `lib/simpleshop_theme_web/controllers/image_controller.ex` - Update thumbnail to serve from disk
|
||||
- `lib/berrypod_web/controllers/image_controller.ex` - Update thumbnail to serve from disk
|
||||
|
||||
**Tests:** Existing controller tests should pass
|
||||
|
||||
@@ -1040,8 +1040,8 @@ ls -la priv/static/mockups/
|
||||
|
||||
6. **Run tests:**
|
||||
```bash
|
||||
mix test test/simpleshop_theme/images/
|
||||
mix test test/simpleshop_theme/workers/
|
||||
mix test test/berrypod/images/
|
||||
mix test test/berrypod/workers/
|
||||
```
|
||||
|
||||
7. **Re-run Lighthouse:**
|
||||
@@ -1063,12 +1063,12 @@ ls -la priv/static/mockups/
|
||||
|
||||
```
|
||||
test/
|
||||
├── simpleshop_theme/
|
||||
├── berrypod/
|
||||
│ ├── images/
|
||||
│ │ └── optimizer_test.exs # Core optimization logic
|
||||
│ └── workers/
|
||||
│ └── image_variants_test.exs # Oban worker
|
||||
├── simpleshop_theme_web/
|
||||
├── berrypod_web/
|
||||
│ └── components/
|
||||
│ └── shop_components_test.exs # responsive_image component
|
||||
├── mix/
|
||||
@@ -1085,16 +1085,16 @@ test/
|
||||
|
||||
**`test/support/fixtures/image_fixtures.ex`:**
|
||||
```elixir
|
||||
defmodule SimpleshopTheme.ImageFixtures do
|
||||
alias SimpleshopTheme.Repo
|
||||
alias SimpleshopTheme.Media.Image
|
||||
defmodule Berrypod.ImageFixtures do
|
||||
alias Berrypod.Repo
|
||||
alias Berrypod.Media.Image
|
||||
|
||||
@sample_jpeg File.read!("test/fixtures/sample_1200x800.jpg")
|
||||
|
||||
def sample_jpeg, do: @sample_jpeg
|
||||
|
||||
def image_fixture(attrs \\ %{}) do
|
||||
{:ok, webp, w, h} = SimpleshopTheme.Images.Optimizer.to_lossless_webp(@sample_jpeg)
|
||||
{:ok, webp, w, h} = Berrypod.Images.Optimizer.to_lossless_webp(@sample_jpeg)
|
||||
|
||||
defaults = %{
|
||||
image_type: "product",
|
||||
@@ -1132,23 +1132,23 @@ defmodule SimpleshopTheme.ImageFixtures do
|
||||
|
||||
def cache_path(id, width, format) do
|
||||
ext = if format == :jpg, do: "jpg", else: Atom.to_string(format)
|
||||
Path.join(SimpleshopTheme.Images.Optimizer.cache_dir(), "#{id}-#{width}.#{ext}")
|
||||
Path.join(Berrypod.Images.Optimizer.cache_dir(), "#{id}-#{width}.#{ext}")
|
||||
end
|
||||
|
||||
def cleanup_cache do
|
||||
cache_dir = SimpleshopTheme.Images.Optimizer.cache_dir()
|
||||
cache_dir = Berrypod.Images.Optimizer.cache_dir()
|
||||
if File.exists?(cache_dir), do: File.rm_rf!(cache_dir)
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
**`test/simpleshop_theme/images/optimizer_test.exs`:**
|
||||
**`test/berrypod/images/optimizer_test.exs`:**
|
||||
```elixir
|
||||
defmodule SimpleshopTheme.Images.OptimizerTest do
|
||||
use SimpleshopTheme.DataCase, async: false
|
||||
defmodule Berrypod.Images.OptimizerTest do
|
||||
use Berrypod.DataCase, async: false
|
||||
|
||||
alias SimpleshopTheme.Images.Optimizer
|
||||
import SimpleshopTheme.ImageFixtures
|
||||
alias Berrypod.Images.Optimizer
|
||||
import Berrypod.ImageFixtures
|
||||
|
||||
setup do
|
||||
cleanup_cache()
|
||||
@@ -1256,15 +1256,15 @@ defmodule SimpleshopTheme.Images.OptimizerTest do
|
||||
end
|
||||
```
|
||||
|
||||
**`test/simpleshop_theme/workers/image_variants_test.exs`:**
|
||||
**`test/berrypod/workers/image_variants_test.exs`:**
|
||||
```elixir
|
||||
defmodule SimpleshopTheme.Workers.ImageVariantsTest do
|
||||
use SimpleshopTheme.DataCase, async: false
|
||||
use Oban.Testing, repo: SimpleshopTheme.Repo
|
||||
defmodule Berrypod.Workers.ImageVariantsTest do
|
||||
use Berrypod.DataCase, async: false
|
||||
use Oban.Testing, repo: Berrypod.Repo
|
||||
|
||||
alias SimpleshopTheme.Workers.ImageVariants
|
||||
alias SimpleshopTheme.Media.Image
|
||||
import SimpleshopTheme.ImageFixtures
|
||||
alias Berrypod.Workers.ImageVariants
|
||||
alias Berrypod.Media.Image
|
||||
import Berrypod.ImageFixtures
|
||||
|
||||
setup do
|
||||
cleanup_cache()
|
||||
@@ -1297,13 +1297,13 @@ defmodule SimpleshopTheme.Workers.ImageVariantsTest do
|
||||
end
|
||||
```
|
||||
|
||||
**`test/simpleshop_theme_web/components/shop_components_test.exs`:**
|
||||
**`test/berrypod_web/components/shop_components_test.exs`:**
|
||||
```elixir
|
||||
defmodule SimpleshopThemeWeb.ShopComponentsTest do
|
||||
use SimpleshopThemeWeb.ConnCase, async: true
|
||||
defmodule BerrypodWeb.ShopComponentsTest do
|
||||
use BerrypodWeb.ConnCase, async: true
|
||||
|
||||
import Phoenix.LiveViewTest
|
||||
alias SimpleshopThemeWeb.ShopComponents
|
||||
alias BerrypodWeb.ShopComponents
|
||||
|
||||
describe "responsive_image/1" do
|
||||
test "builds srcset with all widths for 1200px source" do
|
||||
|
||||
@@ -51,7 +51,7 @@ PageRenderer module
|
||||
### PageLayout Schema
|
||||
|
||||
```elixir
|
||||
defmodule SimpleshopTheme.Content.PageLayout do
|
||||
defmodule Berrypod.Content.PageLayout do
|
||||
use Ecto.Schema
|
||||
|
||||
schema "page_layouts" do
|
||||
@@ -59,7 +59,7 @@ defmodule SimpleshopTheme.Content.PageLayout do
|
||||
field :name, :string # Display name for the layout
|
||||
field :is_default, :boolean, default: false
|
||||
|
||||
has_many :sections, SimpleshopTheme.Content.PageSection
|
||||
has_many :sections, Berrypod.Content.PageSection
|
||||
|
||||
timestamps()
|
||||
end
|
||||
@@ -69,7 +69,7 @@ end
|
||||
### PageSection Schema
|
||||
|
||||
```elixir
|
||||
defmodule SimpleshopTheme.Content.PageSection do
|
||||
defmodule Berrypod.Content.PageSection do
|
||||
use Ecto.Schema
|
||||
|
||||
schema "page_sections" do
|
||||
@@ -78,7 +78,7 @@ defmodule SimpleshopTheme.Content.PageSection do
|
||||
field :settings, :map # JSON settings for the section
|
||||
field :enabled, :boolean, default: true
|
||||
|
||||
belongs_to :page_layout, SimpleshopTheme.Content.PageLayout
|
||||
belongs_to :page_layout, Berrypod.Content.PageLayout
|
||||
|
||||
timestamps()
|
||||
end
|
||||
@@ -88,11 +88,11 @@ end
|
||||
### Section Types Registry
|
||||
|
||||
```elixir
|
||||
defmodule SimpleshopTheme.Content.SectionTypes do
|
||||
defmodule Berrypod.Content.SectionTypes do
|
||||
@sections %{
|
||||
"hero" => %{
|
||||
name: "Hero Banner",
|
||||
component: &SimpleshopThemeWeb.ShopComponents.hero_section/1,
|
||||
component: &BerrypodWeb.ShopComponents.hero_section/1,
|
||||
settings_schema: %{
|
||||
title: %{type: :string, default: "Welcome"},
|
||||
description: %{type: :string, default: ""},
|
||||
@@ -104,7 +104,7 @@ defmodule SimpleshopTheme.Content.SectionTypes do
|
||||
},
|
||||
"featured_products" => %{
|
||||
name: "Featured Products",
|
||||
component: &SimpleshopThemeWeb.ShopComponents.featured_products_section/1,
|
||||
component: &BerrypodWeb.ShopComponents.featured_products_section/1,
|
||||
settings_schema: %{
|
||||
title: %{type: :string, default: "Featured products"},
|
||||
product_count: %{type: :integer, default: 8}
|
||||
@@ -113,13 +113,13 @@ defmodule SimpleshopTheme.Content.SectionTypes do
|
||||
},
|
||||
"category_nav" => %{
|
||||
name: "Category Navigation",
|
||||
component: &SimpleshopThemeWeb.ShopComponents.category_nav/1,
|
||||
component: &BerrypodWeb.ShopComponents.category_nav/1,
|
||||
settings_schema: %{},
|
||||
allowed_on: [:home]
|
||||
},
|
||||
"image_text" => %{
|
||||
name: "Image + Text Block",
|
||||
component: &SimpleshopThemeWeb.ShopComponents.image_text_section/1,
|
||||
component: &BerrypodWeb.ShopComponents.image_text_section/1,
|
||||
settings_schema: %{
|
||||
title: %{type: :string},
|
||||
description: %{type: :text},
|
||||
@@ -131,7 +131,7 @@ defmodule SimpleshopTheme.Content.SectionTypes do
|
||||
},
|
||||
"content_body" => %{
|
||||
name: "Rich Text Content",
|
||||
component: &SimpleshopThemeWeb.ShopComponents.content_body/1,
|
||||
component: &BerrypodWeb.ShopComponents.content_body/1,
|
||||
settings_schema: %{
|
||||
image_url: %{type: :image},
|
||||
content: %{type: :rich_text}
|
||||
@@ -140,13 +140,13 @@ defmodule SimpleshopTheme.Content.SectionTypes do
|
||||
},
|
||||
"reviews_section" => %{
|
||||
name: "Customer Reviews",
|
||||
component: &SimpleshopThemeWeb.ShopComponents.reviews_section/1,
|
||||
component: &BerrypodWeb.ShopComponents.reviews_section/1,
|
||||
settings_schema: %{},
|
||||
allowed_on: [:pdp]
|
||||
},
|
||||
"related_products" => %{
|
||||
name: "Related Products",
|
||||
component: &SimpleshopThemeWeb.ShopComponents.related_products_section/1,
|
||||
component: &BerrypodWeb.ShopComponents.related_products_section/1,
|
||||
settings_schema: %{},
|
||||
allowed_on: [:pdp]
|
||||
}
|
||||
@@ -157,11 +157,11 @@ end
|
||||
## Page Renderer
|
||||
|
||||
```elixir
|
||||
defmodule SimpleshopThemeWeb.PageRenderer do
|
||||
defmodule BerrypodWeb.PageRenderer do
|
||||
use Phoenix.Component
|
||||
import SimpleshopThemeWeb.ShopComponents
|
||||
import BerrypodWeb.ShopComponents
|
||||
|
||||
alias SimpleshopTheme.Content.SectionTypes
|
||||
alias Berrypod.Content.SectionTypes
|
||||
|
||||
@doc """
|
||||
Renders a page from its layout and data context.
|
||||
@@ -394,6 +394,6 @@ Page layouts should be cached since they change infrequently:
|
||||
|
||||
## Related Files
|
||||
|
||||
- `lib/simpleshop_theme_web/components/shop_components.ex` - Existing section components
|
||||
- `lib/simpleshop_theme_web/components/page_templates/` - Current static templates (will become defaults)
|
||||
- `lib/simpleshop_theme_web/live/theme_live/index.ex` - Theme editor (reference implementation)
|
||||
- `lib/berrypod_web/components/shop_components.ex` - Existing section components
|
||||
- `lib/berrypod_web/components/page_templates/` - Current static templates (will become defaults)
|
||||
- `lib/berrypod_web/live/theme_live/index.ex` - Theme editor (reference implementation)
|
||||
|
||||
@@ -36,7 +36,7 @@ This simplification means `provider_data` stores different fields and shipping r
|
||||
|
||||
### 1.1 HTTP client
|
||||
|
||||
**New file:** `lib/simpleshop_theme/clients/printful.ex`
|
||||
**New file:** `lib/berrypod/clients/printful.ex`
|
||||
|
||||
Same pattern as `clients/printify.ex`. Uses `Req` with Bearer token auth.
|
||||
|
||||
@@ -70,7 +70,7 @@ API key stored in process dictionary (`Process.put(:printful_api_key, key)`) sam
|
||||
|
||||
### 1.2 Provider implementation
|
||||
|
||||
**New file:** `lib/simpleshop_theme/providers/printful.ex`
|
||||
**New file:** `lib/berrypod/providers/printful.ex`
|
||||
|
||||
Implements the `Provider` behaviour. The big difference from Printify is that Printful products don't need a "shop" — they come from the global catalogue. But orders require a store_id.
|
||||
|
||||
@@ -173,16 +173,16 @@ Alternatively, query rates for all countries in `Shipping.@country_names` to pop
|
||||
|
||||
### 1.3 Wire into Provider dispatch
|
||||
|
||||
**File:** `lib/simpleshop_theme/providers/provider.ex`
|
||||
**File:** `lib/berrypod/providers/provider.ex`
|
||||
|
||||
Change line 97:
|
||||
```elixir
|
||||
defp default_for_type("printful"), do: {:ok, SimpleshopTheme.Providers.Printful}
|
||||
defp default_for_type("printful"), do: {:ok, Berrypod.Providers.Printful}
|
||||
```
|
||||
|
||||
### 1.4 Order submission: multi-provider routing
|
||||
|
||||
**File:** `lib/simpleshop_theme/orders.ex`
|
||||
**File:** `lib/berrypod/orders.ex`
|
||||
|
||||
Currently `get_provider_connection/0` is hardcoded to `"printify"`. For multi-provider support, orders need to route to the correct provider based on which connection owns the product.
|
||||
|
||||
@@ -223,7 +223,7 @@ Already covered in the client (1.1). The flow:
|
||||
|
||||
### 2.2 Mockup worker
|
||||
|
||||
**New file:** `lib/simpleshop_theme/sync/mockup_generation_worker.ex`
|
||||
**New file:** `lib/berrypod/sync/mockup_generation_worker.ex`
|
||||
|
||||
Oban worker that generates mockups for a product after sync:
|
||||
|
||||
@@ -294,7 +294,7 @@ This works correctly with the existing calculation logic — no changes needed t
|
||||
|
||||
### 4.1 Webhook handler
|
||||
|
||||
**New file:** `lib/simpleshop_theme_web/controllers/printful_webhook_controller.ex`
|
||||
**New file:** `lib/berrypod_web/controllers/printful_webhook_controller.ex`
|
||||
|
||||
Printful webhooks POST JSON to a configured URL. Events we care about:
|
||||
|
||||
@@ -353,22 +353,22 @@ Identical to Printify — the admin clicks "Sync" and the ProductSyncWorker runs
|
||||
### New files (6)
|
||||
| File | Purpose |
|
||||
|------|---------|
|
||||
| `lib/simpleshop_theme/clients/printful.ex` | HTTP client |
|
||||
| `lib/simpleshop_theme/providers/printful.ex` | Provider behaviour implementation |
|
||||
| `lib/simpleshop_theme/sync/mockup_generation_worker.ex` | Async mockup generation |
|
||||
| `lib/simpleshop_theme_web/controllers/printful_webhook_controller.ex` | Webhook handler |
|
||||
| `test/simpleshop_theme/providers/printful_test.exs` | Provider tests |
|
||||
| `test/simpleshop_theme/clients/printful_test.exs` | Client tests (with Req mocking) |
|
||||
| `lib/berrypod/clients/printful.ex` | HTTP client |
|
||||
| `lib/berrypod/providers/printful.ex` | Provider behaviour implementation |
|
||||
| `lib/berrypod/sync/mockup_generation_worker.ex` | Async mockup generation |
|
||||
| `lib/berrypod_web/controllers/printful_webhook_controller.ex` | Webhook handler |
|
||||
| `test/berrypod/providers/printful_test.exs` | Provider tests |
|
||||
| `test/berrypod/clients/printful_test.exs` | Client tests (with Req mocking) |
|
||||
|
||||
### Modified files (6)
|
||||
| File | Change |
|
||||
|------|--------|
|
||||
| `lib/simpleshop_theme/providers/provider.ex` | Wire `"printful"` to Printful module |
|
||||
| `lib/simpleshop_theme/orders.ex` | Route to correct provider per order (not hardcoded "printify") |
|
||||
| `lib/simpleshop_theme_web/router.ex` | Add Printful webhook route |
|
||||
| `lib/berrypod/providers/provider.ex` | Wire `"printful"` to Printful module |
|
||||
| `lib/berrypod/orders.ex` | Route to correct provider per order (not hardcoded "printify") |
|
||||
| `lib/berrypod_web/router.ex` | Add Printful webhook route |
|
||||
| `config/config.exs` | (optional) Printful-specific config |
|
||||
| `lib/simpleshop_theme/sync/product_sync_worker.ex` | Enqueue mockup generation for Printful products |
|
||||
| `lib/simpleshop_theme_web/live/admin/settings.ex` | Provider form tweaks for Printful |
|
||||
| `lib/berrypod/sync/product_sync_worker.ex` | Enqueue mockup generation for Printful products |
|
||||
| `lib/berrypod_web/live/admin/settings.ex` | Provider form tweaks for Printful |
|
||||
|
||||
---
|
||||
|
||||
@@ -399,7 +399,7 @@ Identical to Printify — the admin clicks "Sync" and the ProductSyncWorker runs
|
||||
|
||||
## Decisions and trade-offs
|
||||
|
||||
**Using sync products (not catalogue browsing):** Printful's model expects sellers to set up products in Printful's dashboard first (apply designs, choose products, set pricing). SimpleShop syncs those configured products. This matches the Printify workflow where products exist in the provider's system first. Full catalogue browsing + product creation via API is possible but significantly more complex (need artwork upload, placement positioning, pricing config) — better suited for a v2.
|
||||
**Using sync products (not catalogue browsing):** Printful's model expects sellers to set up products in Printful's dashboard first (apply designs, choose products, set pricing). Berrypod syncs those configured products. This matches the Printify workflow where products exist in the provider's system first. Full catalogue browsing + product creation via API is possible but significantly more complex (need artwork upload, placement positioning, pricing config) — better suited for a v2.
|
||||
|
||||
**Reusing ShippingRate schema fields:** `blueprint_id` and `print_provider_id` are Printify-specific names, but they serve as generic "product type ID" and "provider ID" slots. Renaming them would be a migration + touch every query. Not worth it until a third provider makes the naming confusing.
|
||||
|
||||
|
||||
@@ -74,17 +74,17 @@ The official Shopify integration handles product identity well through SKUs.
|
||||
|
||||
- Apply at printify.com/printify-api
|
||||
- ~1 week approval process
|
||||
- Merchants authorize SimpleShop via OAuth
|
||||
- Merchants authorize Berrypod via OAuth
|
||||
- We appear in Printify's publishing UI alongside Shopify/Etsy
|
||||
- Products get "published" TO us with lock
|
||||
- Webhooks for real-time updates
|
||||
|
||||
**How publishing works in Option 2:**
|
||||
1. Merchant clicks "Publish to SimpleShop" in Printify
|
||||
1. Merchant clicks "Publish to Berrypod" in Printify
|
||||
2. Printify sends `product:publish:started` webhook
|
||||
3. We create the product on our side
|
||||
4. We call `publishing_succeeded.json` to confirm
|
||||
5. Product locked in Printify, shows as "Published to SimpleShop"
|
||||
5. Product locked in Printify, shows as "Published to Berrypod"
|
||||
|
||||
**Benefits of official integration:**
|
||||
- Publish lock prevents editing after publishing (data consistency)
|
||||
@@ -189,7 +189,7 @@ From Printify documentation:
|
||||
|
||||
### The core tension
|
||||
|
||||
SimpleShop exists in two forms:
|
||||
Berrypod exists in two forms:
|
||||
1. **Open source** - self-hosted by anyone
|
||||
2. **Managed hosting** - SaaS service run by us
|
||||
|
||||
@@ -199,7 +199,7 @@ Printify's integration options have different implications for each.
|
||||
|
||||
**How it works:**
|
||||
- Each merchant creates their own Printify API token
|
||||
- Token entered in SimpleShop admin
|
||||
- Token entered in Berrypod admin
|
||||
- Direct API access, no intermediary
|
||||
|
||||
**Open source:** ✅ Works perfectly
|
||||
@@ -222,7 +222,7 @@ Printify's integration options have different implications for each.
|
||||
### OAuth Platform Integration
|
||||
|
||||
**How it works:**
|
||||
- Register SimpleShop as an app with Printify
|
||||
- Register Berrypod as an app with Printify
|
||||
- Get OAuth client ID/secret
|
||||
- Merchants click "Connect" and authorize
|
||||
- We receive access tokens, appear in Printify UI
|
||||
@@ -270,7 +270,7 @@ Printify's integration options have different implications for each.
|
||||
| Webhook proxy | Depends on proxy | Full features |
|
||||
|
||||
**Potential differentiation for managed hosting:**
|
||||
- Official "Publish to SimpleShop" in Printify UI
|
||||
- Official "Publish to Berrypod" in Printify UI
|
||||
- Real-time sync via webhooks
|
||||
- Publish lock (data consistency guarantee)
|
||||
- Pre-checkout validation (verify before order)
|
||||
|
||||
@@ -14,7 +14,7 @@ Build a Products context that syncs products from external POD providers (Printi
|
||||
|
||||
## Current Domain Analysis
|
||||
|
||||
SimpleShop has **6 well-defined domains** with clear boundaries:
|
||||
Berrypod has **6 well-defined domains** with clear boundaries:
|
||||
|
||||
| Domain | Purpose | Schemas | Public Functions |
|
||||
|--------|---------|---------|------------------|
|
||||
@@ -36,12 +36,12 @@ The new **Products** context will be a new top-level domain that:
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────────────────┐
|
||||
│ SimpleshopTheme │
|
||||
│ Berrypod │
|
||||
├─────────────────────────────────────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ ┌──────────────────────────────────────────────────────────────────────┐ │
|
||||
│ │ WEB LAYER │ │
|
||||
│ │ SimpleshopThemeWeb │ │
|
||||
│ │ BerrypodWeb │ │
|
||||
│ │ ┌────────────────┐ ┌────────────────┐ ┌─────────────────────────┐ │ │
|
||||
│ │ │ Shop LiveViews │ │ Admin LiveViews│ │ Theme Editor LiveView │ │ │
|
||||
│ │ │ - ProductShow │ │ - UserLogin │ │ - ThemeLive.Index │ │ │
|
||||
@@ -299,22 +299,22 @@ Extract reusable helpers into new `Printify.Catalog` module:
|
||||
|
||||
**Module structure:**
|
||||
```
|
||||
lib/simpleshop_theme/clients/
|
||||
lib/berrypod/clients/
|
||||
├── printify.ex # Printify HTTP client (moved from printify/client.ex)
|
||||
├── gelato.ex # Gelato HTTP client
|
||||
└── prodigi.ex # Prodigi HTTP client
|
||||
|
||||
lib/simpleshop_theme/providers/
|
||||
lib/berrypod/providers/
|
||||
├── provider.ex # Behaviour definition
|
||||
├── printify.ex # Printify implementation (uses Clients.Printify)
|
||||
├── gelato.ex # Gelato implementation (uses Clients.Gelato)
|
||||
└── prodigi.ex # Prodigi implementation (uses Clients.Prodigi)
|
||||
|
||||
lib/simpleshop_theme/mockups/
|
||||
lib/berrypod/mockups/
|
||||
└── generator.ex # Mockup generation (currently uses Clients.Printify)
|
||||
# Provider-agnostic location for future flexibility
|
||||
|
||||
lib/simpleshop_theme/printify/
|
||||
lib/berrypod/printify/
|
||||
└── catalog.ex # Blueprint/variant discovery helpers (Printify-specific)
|
||||
```
|
||||
|
||||
@@ -343,7 +343,7 @@ Each provider module uses its corresponding client. The mockup generator is in a
|
||||
- Unique constraint: `[:provider_connection_id, :provider_product_id]`
|
||||
|
||||
2. **Credentials encrypted in database**
|
||||
- Use `SimpleshopTheme.Vault` for at-rest encryption
|
||||
- Use `Berrypod.Vault` for at-rest encryption
|
||||
- `api_key_encrypted`, `oauth_access_token_encrypted` fields
|
||||
|
||||
3. **Cost tracking for profit calculation**
|
||||
@@ -832,46 +832,46 @@ Add to `mix.exs`:
|
||||
- `*_create_admin_notifications.exs`
|
||||
|
||||
### Schemas
|
||||
- `lib/simpleshop_theme/products/provider_connection.ex`
|
||||
- `lib/simpleshop_theme/products/product.ex`
|
||||
- `lib/simpleshop_theme/products/product_image.ex`
|
||||
- `lib/simpleshop_theme/products/product_variant.ex`
|
||||
- `lib/simpleshop_theme/orders/order.ex`
|
||||
- `lib/simpleshop_theme/orders/order_fulfillment.ex`
|
||||
- `lib/simpleshop_theme/orders/order_line_item.ex`
|
||||
- `lib/simpleshop_theme/orders/order_event.ex`
|
||||
- `lib/simpleshop_theme/admin_notifications/notification.ex`
|
||||
- `lib/berrypod/products/provider_connection.ex`
|
||||
- `lib/berrypod/products/product.ex`
|
||||
- `lib/berrypod/products/product_image.ex`
|
||||
- `lib/berrypod/products/product_variant.ex`
|
||||
- `lib/berrypod/orders/order.ex`
|
||||
- `lib/berrypod/orders/order_fulfillment.ex`
|
||||
- `lib/berrypod/orders/order_line_item.ex`
|
||||
- `lib/berrypod/orders/order_event.ex`
|
||||
- `lib/berrypod/admin_notifications/notification.ex`
|
||||
|
||||
### Contexts
|
||||
- `lib/simpleshop_theme/products.ex` - Product queries, sync logic
|
||||
- `lib/simpleshop_theme/orders.ex` - Order creation, submission
|
||||
- `lib/simpleshop_theme/admin_notifications.ex` - Admin notification management
|
||||
- `lib/berrypod/products.ex` - Product queries, sync logic
|
||||
- `lib/berrypod/orders.ex` - Order creation, submission
|
||||
- `lib/berrypod/admin_notifications.ex` - Admin notification management
|
||||
|
||||
### Providers
|
||||
- `lib/simpleshop_theme/providers/provider.ex` - Behaviour definition
|
||||
- `lib/simpleshop_theme/providers/printify.ex` - Printify implementation
|
||||
- `lib/berrypod/providers/provider.ex` - Behaviour definition
|
||||
- `lib/berrypod/providers/printify.ex` - Printify implementation
|
||||
|
||||
### Workers
|
||||
- `lib/simpleshop_theme/sync/product_sync_worker.ex` - Oban worker
|
||||
- `lib/berrypod/sync/product_sync_worker.ex` - Oban worker
|
||||
|
||||
### Webhooks
|
||||
- `lib/simpleshop_theme_web/controllers/webhook_controller.ex`
|
||||
- `lib/simpleshop_theme/webhooks/printify_handler.ex`
|
||||
- `lib/berrypod_web/controllers/webhook_controller.ex`
|
||||
- `lib/berrypod/webhooks/printify_handler.ex`
|
||||
|
||||
### Notifiers
|
||||
- `lib/simpleshop_theme_web/notifiers/customer_notifier.ex` - Customer emails
|
||||
- `lib/berrypod_web/notifiers/customer_notifier.ex` - Customer emails
|
||||
|
||||
### Support
|
||||
- `lib/simpleshop_theme/vault.ex` - Credential encryption
|
||||
- `lib/berrypod/vault.ex` - Credential encryption
|
||||
|
||||
---
|
||||
|
||||
## Files to Modify
|
||||
|
||||
- `lib/simpleshop_theme/printify/client.ex` → Move to `lib/simpleshop_theme/clients/printify.ex`
|
||||
- `lib/simpleshop_theme/printify/mockup_generator.ex` → Move to `lib/simpleshop_theme/mockups/generator.ex`
|
||||
- `lib/simpleshop_theme/theme/preview_data.ex` - Query real products when available
|
||||
- `lib/simpleshop_theme_web/live/shop_live/*.ex` - Use Products context instead of PreviewData
|
||||
- `lib/berrypod/printify/client.ex` → Move to `lib/berrypod/clients/printify.ex`
|
||||
- `lib/berrypod/printify/mockup_generator.ex` → Move to `lib/berrypod/mockups/generator.ex`
|
||||
- `lib/berrypod/theme/preview_data.ex` - Query real products when available
|
||||
- `lib/berrypod_web/live/shop_live/*.ex` - Use Products context instead of PreviewData
|
||||
|
||||
---
|
||||
|
||||
@@ -943,10 +943,10 @@ end
|
||||
```
|
||||
|
||||
### Files to Create
|
||||
- `lib/simpleshop_theme/admin_notifications.ex` - Admin notification context
|
||||
- `lib/simpleshop_theme/admin_notifications/notification.ex` - Schema
|
||||
- `lib/simpleshop_theme/orders/order_event.ex` - Customer-facing event schema
|
||||
- `lib/simpleshop_theme_web/notifiers/customer_notifier.ex` - Emails
|
||||
- `lib/berrypod/admin_notifications.ex` - Admin notification context
|
||||
- `lib/berrypod/admin_notifications/notification.ex` - Schema
|
||||
- `lib/berrypod/orders/order_event.ex` - Customer-facing event schema
|
||||
- `lib/berrypod_web/notifiers/customer_notifier.ex` - Emails
|
||||
|
||||
---
|
||||
|
||||
@@ -1012,15 +1012,15 @@ end
|
||||
**Mocking External APIs (Mox pattern):**
|
||||
```elixir
|
||||
# test/support/mocks.ex
|
||||
Mox.defmock(SimpleshopTheme.Clients.MockPrintify, for: SimpleshopTheme.Clients.PrintifyBehaviour)
|
||||
Mox.defmock(Berrypod.Clients.MockPrintify, for: Berrypod.Clients.PrintifyBehaviour)
|
||||
|
||||
# config/test.exs
|
||||
config :simpleshop_theme, :printify_client, SimpleshopTheme.Clients.MockPrintify
|
||||
config :berrypod, :printify_client, Berrypod.Clients.MockPrintify
|
||||
```
|
||||
|
||||
**Oban testing:**
|
||||
```elixir
|
||||
use Oban.Testing, repo: SimpleshopTheme.Repo
|
||||
use Oban.Testing, repo: Berrypod.Repo
|
||||
# Jobs run synchronously in tests via perform_job/2
|
||||
```
|
||||
|
||||
@@ -1049,7 +1049,7 @@ use Oban.Testing, repo: SimpleshopTheme.Repo
|
||||
### Example Test Cases
|
||||
|
||||
```elixir
|
||||
# test/simpleshop_theme/products_test.exs
|
||||
# test/berrypod/products_test.exs
|
||||
describe "sync_products/1" do
|
||||
test "syncs products from provider" do
|
||||
conn = provider_connection_fixture()
|
||||
@@ -1082,7 +1082,7 @@ describe "sync_products/1" do
|
||||
end
|
||||
end
|
||||
|
||||
# test/simpleshop_theme/orders_test.exs
|
||||
# test/berrypod/orders_test.exs
|
||||
describe "create_order_from_cart/1" do
|
||||
test "splits cart into fulfillments by provider" do
|
||||
printify_variant = variant_fixture(provider: :printify)
|
||||
@@ -1098,7 +1098,7 @@ describe "create_order_from_cart/1" do
|
||||
end
|
||||
end
|
||||
|
||||
# test/simpleshop_theme/sync/product_sync_worker_test.exs
|
||||
# test/berrypod/sync/product_sync_worker_test.exs
|
||||
describe "perform/1" do
|
||||
test "retries on API failure" do
|
||||
expect(MockPrintify, :list_products, fn _ -> {:error, :timeout} end)
|
||||
@@ -1197,12 +1197,12 @@ live "/admin/providers/:id/edit", ProviderLive.Index, :edit
|
||||
|
||||
| File | Purpose |
|
||||
|------|---------|
|
||||
| `lib/simpleshop_theme_web/live/provider_live/index.ex` | LiveView for provider list + modal forms |
|
||||
| `lib/simpleshop_theme_web/live/provider_live/index.html.heex` | Template |
|
||||
| `lib/simpleshop_theme_web/live/provider_live/form_component.ex` | Form component for new/edit |
|
||||
| `lib/simpleshop_theme/providers/printify.ex` | Add `register_webhooks/1`, `unregister_webhooks/1` |
|
||||
| `lib/simpleshop_theme/workers/product_sync_worker.ex` | Stub for "Sync Now" (full impl in next task) |
|
||||
| `test/simpleshop_theme_web/live/provider_live_test.exs` | LiveView tests |
|
||||
| `lib/berrypod_web/live/provider_live/index.ex` | LiveView for provider list + modal forms |
|
||||
| `lib/berrypod_web/live/provider_live/index.html.heex` | Template |
|
||||
| `lib/berrypod_web/live/provider_live/form_component.ex` | Form component for new/edit |
|
||||
| `lib/berrypod/providers/printify.ex` | Add `register_webhooks/1`, `unregister_webhooks/1` |
|
||||
| `lib/berrypod/workers/product_sync_worker.ex` | Stub for "Sync Now" (full impl in next task) |
|
||||
| `test/berrypod_web/live/provider_live_test.exs` | LiveView tests |
|
||||
|
||||
### UI Design
|
||||
|
||||
@@ -1264,11 +1264,11 @@ Single-page admin with modal for add/edit (follows Phoenix generator pattern):
|
||||
**Index LiveView (`provider_live/index.ex`):**
|
||||
|
||||
```elixir
|
||||
defmodule SimpleshopThemeWeb.ProviderLive.Index do
|
||||
use SimpleshopThemeWeb, :live_view
|
||||
defmodule BerrypodWeb.ProviderLive.Index do
|
||||
use BerrypodWeb, :live_view
|
||||
|
||||
alias SimpleshopTheme.Products
|
||||
alias SimpleshopTheme.Products.ProviderConnection
|
||||
alias Berrypod.Products
|
||||
alias Berrypod.Products.ProviderConnection
|
||||
|
||||
@impl true
|
||||
def mount(_params, _session, socket) do
|
||||
@@ -1300,7 +1300,7 @@ defmodule SimpleshopThemeWeb.ProviderLive.Index do
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_info({SimpleshopThemeWeb.ProviderLive.FormComponent, {:saved, connection}}, socket) do
|
||||
def handle_info({BerrypodWeb.ProviderLive.FormComponent, {:saved, connection}}, socket) do
|
||||
{:noreply, stream_insert(socket, :connections, connection)}
|
||||
end
|
||||
|
||||
@@ -1351,7 +1351,7 @@ end
|
||||
defp test_provider_connection("printify", api_key) do
|
||||
# Build temporary connection struct for testing
|
||||
conn = %ProviderConnection{provider_type: "printify", api_key: api_key}
|
||||
SimpleshopTheme.Providers.Printify.test_connection(conn)
|
||||
Berrypod.Providers.Printify.test_connection(conn)
|
||||
end
|
||||
```
|
||||
|
||||
@@ -1404,7 +1404,7 @@ def handle_event("save", %{"provider" => params}, socket) do
|
||||
end
|
||||
|
||||
defp register_webhooks(%{provider_type: "printify"} = conn) do
|
||||
SimpleshopTheme.Providers.Printify.register_webhooks(conn)
|
||||
Berrypod.Providers.Printify.register_webhooks(conn)
|
||||
end
|
||||
```
|
||||
|
||||
@@ -1422,7 +1422,7 @@ def handle_event("delete", %{"id" => id}, socket) do
|
||||
end
|
||||
|
||||
defp unregister_webhooks(%{provider_type: "printify"} = conn) do
|
||||
SimpleshopTheme.Providers.Printify.unregister_webhooks(conn)
|
||||
Berrypod.Providers.Printify.unregister_webhooks(conn)
|
||||
end
|
||||
```
|
||||
|
||||
@@ -1432,7 +1432,7 @@ end
|
||||
@webhook_topics ~w(product:publish:started product:deleted shop:disconnected)
|
||||
|
||||
def register_webhooks(conn) do
|
||||
webhook_url = SimpleshopThemeWeb.Endpoint.url() <> "/webhooks/printify"
|
||||
webhook_url = BerrypodWeb.Endpoint.url() <> "/webhooks/printify"
|
||||
shop_id = get_shop_id(conn)
|
||||
|
||||
results = Enum.map(@webhook_topics, fn topic ->
|
||||
@@ -1455,7 +1455,7 @@ def unregister_webhooks(conn) do
|
||||
# List existing webhooks and delete ours
|
||||
case Client.get(conn, "/shops/#{shop_id}/webhooks.json") do
|
||||
{:ok, %{"webhooks" => webhooks}} ->
|
||||
our_url = SimpleshopThemeWeb.Endpoint.url() <> "/webhooks/printify"
|
||||
our_url = BerrypodWeb.Endpoint.url() <> "/webhooks/printify"
|
||||
|
||||
webhooks
|
||||
|> Enum.filter(&(&1["url"] == our_url))
|
||||
@@ -1504,7 +1504,7 @@ Template snippet:
|
||||
|
||||
### Context Additions
|
||||
|
||||
Add to `lib/simpleshop_theme/products.ex`:
|
||||
Add to `lib/berrypod/products.ex`:
|
||||
|
||||
```elixir
|
||||
@doc """
|
||||
@@ -1513,7 +1513,7 @@ Returns {:ok, job} or {:error, changeset}.
|
||||
"""
|
||||
def enqueue_sync(%ProviderConnection{} = conn) do
|
||||
%{connection_id: conn.id}
|
||||
|> SimpleshopTheme.Workers.ProductSyncWorker.new()
|
||||
|> Berrypod.Workers.ProductSyncWorker.new()
|
||||
|> Oban.insert()
|
||||
end
|
||||
|
||||
@@ -1543,7 +1543,7 @@ end
|
||||
### Testing
|
||||
|
||||
```elixir
|
||||
# test/simpleshop_theme_web/live/provider_live_test.exs
|
||||
# test/berrypod_web/live/provider_live_test.exs
|
||||
describe "Index" do
|
||||
setup :register_and_log_in_user
|
||||
|
||||
@@ -1619,25 +1619,25 @@ Implement a robust product sync strategy with three mechanisms:
|
||||
|
||||
| File | Purpose |
|
||||
|------|---------|
|
||||
| `lib/simpleshop_theme/workers/product_sync_worker.ex` | Oban worker for full/single product sync |
|
||||
| `lib/simpleshop_theme_web/controllers/webhook_controller.ex` | Receives webhooks from providers |
|
||||
| `lib/simpleshop_theme/webhooks/printify_handler.ex` | Printify-specific webhook processing |
|
||||
| `test/simpleshop_theme/workers/product_sync_worker_test.exs` | Worker tests |
|
||||
| `test/simpleshop_theme_web/controllers/webhook_controller_test.exs` | Webhook endpoint tests |
|
||||
| `lib/berrypod/workers/product_sync_worker.ex` | Oban worker for full/single product sync |
|
||||
| `lib/berrypod_web/controllers/webhook_controller.ex` | Receives webhooks from providers |
|
||||
| `lib/berrypod/webhooks/printify_handler.ex` | Printify-specific webhook processing |
|
||||
| `test/berrypod/workers/product_sync_worker_test.exs` | Worker tests |
|
||||
| `test/berrypod_web/controllers/webhook_controller_test.exs` | Webhook endpoint tests |
|
||||
|
||||
### Part 1: ProductSyncWorker (~1hr)
|
||||
|
||||
Oban worker that syncs products from a provider connection.
|
||||
|
||||
```elixir
|
||||
defmodule SimpleshopTheme.Workers.ProductSyncWorker do
|
||||
defmodule Berrypod.Workers.ProductSyncWorker do
|
||||
use Oban.Worker,
|
||||
queue: :sync,
|
||||
max_attempts: 3,
|
||||
unique: [period: 60, fields: [:args, :queue]]
|
||||
|
||||
alias SimpleshopTheme.Products
|
||||
alias SimpleshopTheme.Providers
|
||||
alias Berrypod.Products
|
||||
alias Berrypod.Providers
|
||||
|
||||
@impl Oban.Worker
|
||||
def perform(%Oban.Job{args: %{"connection_id" => conn_id} = args}) do
|
||||
@@ -1714,10 +1714,10 @@ post "/webhooks/printify", WebhookController, :printify
|
||||
|
||||
**Controller:**
|
||||
```elixir
|
||||
defmodule SimpleshopThemeWeb.WebhookController do
|
||||
use SimpleshopThemeWeb, :controller
|
||||
defmodule BerrypodWeb.WebhookController do
|
||||
use BerrypodWeb, :controller
|
||||
|
||||
alias SimpleshopTheme.Webhooks.PrintifyHandler
|
||||
alias Berrypod.Webhooks.PrintifyHandler
|
||||
|
||||
def printify(conn, params) do
|
||||
with :ok <- verify_printify_signature(conn),
|
||||
@@ -1737,7 +1737,7 @@ defmodule SimpleshopThemeWeb.WebhookController do
|
||||
# Header: X-Printify-Signature
|
||||
signature = get_req_header(conn, "x-printify-signature") |> List.first()
|
||||
body = conn.assigns[:raw_body]
|
||||
secret = Application.get_env(:simpleshop_theme, :printify_webhook_secret)
|
||||
secret = Application.get_env(:berrypod, :printify_webhook_secret)
|
||||
|
||||
expected = :crypto.mac(:hmac, :sha256, secret, body) |> Base.encode16(case: :lower)
|
||||
|
||||
@@ -1752,9 +1752,9 @@ end
|
||||
|
||||
**Handler:**
|
||||
```elixir
|
||||
defmodule SimpleshopTheme.Webhooks.PrintifyHandler do
|
||||
alias SimpleshopTheme.Products
|
||||
alias SimpleshopTheme.Workers.ProductSyncWorker
|
||||
defmodule Berrypod.Webhooks.PrintifyHandler do
|
||||
alias Berrypod.Products
|
||||
alias Berrypod.Workers.ProductSyncWorker
|
||||
|
||||
def handle(%{"type" => "product:publish:started", "resource" => resource}) do
|
||||
%{"shop_id" => shop_id, "id" => product_id} = resource
|
||||
@@ -1800,16 +1800,16 @@ end
|
||||
Add to Oban config for daily fallback:
|
||||
```elixir
|
||||
# In config/config.exs
|
||||
config :simpleshop_theme, Oban,
|
||||
config :berrypod, Oban,
|
||||
plugins: [
|
||||
{Oban.Plugins.Cron, crontab: [
|
||||
{"0 3 * * *", SimpleshopTheme.Workers.ScheduledSyncWorker} # 3 AM daily
|
||||
{"0 3 * * *", Berrypod.Workers.ScheduledSyncWorker} # 3 AM daily
|
||||
]}
|
||||
]
|
||||
```
|
||||
|
||||
```elixir
|
||||
defmodule SimpleshopTheme.Workers.ScheduledSyncWorker do
|
||||
defmodule Berrypod.Workers.ScheduledSyncWorker do
|
||||
use Oban.Worker, queue: :sync
|
||||
|
||||
def perform(_job) do
|
||||
@@ -1827,7 +1827,7 @@ end
|
||||
|
||||
### Context Additions
|
||||
|
||||
Add to `lib/simpleshop_theme/products.ex`:
|
||||
Add to `lib/berrypod/products.ex`:
|
||||
|
||||
```elixir
|
||||
def archive_product_by_provider(connection_id, provider_product_id) do
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
**Status:** Reference (research notes from Feb 2025)
|
||||
|
||||
Research session exploring multi-provider strategy for SimpleShop. Goal: identify the best additional POD providers to complement Printify, with a focus on UK fulfilment coverage.
|
||||
Research session exploring multi-provider strategy for Berrypod. Goal: identify the best additional POD providers to complement Printify, with a focus on UK fulfilment coverage.
|
||||
|
||||
---
|
||||
|
||||
@@ -104,7 +104,7 @@ Net cost is comparable across providers for UK delivery. Prodigi's higher base c
|
||||
|
||||
### Phase 1: Add Printful (revised winner)
|
||||
|
||||
Original analysis recommended Prodigi for UK coverage (9/10 types). But Prodigi has a critical gap: **no mockup generation API**. Sellers would need to manually create and upload product mockups, which is too much friction for SimpleShop's target audience.
|
||||
Original analysis recommended Prodigi for UK coverage (9/10 types). But Prodigi has a critical gap: **no mockup generation API**. Sellers would need to manually create and upload product mockups, which is too much friction for Berrypod's target audience.
|
||||
|
||||
Printful wins because:
|
||||
- **Mockup generation API** — dedicated async endpoint, generates mockups on actual product blanks
|
||||
@@ -166,7 +166,7 @@ The existing codebase already supports multi-provider through:
|
||||
- **`Provider.for_type/1`** dispatch (currently Printify-only, extensible via app env)
|
||||
|
||||
Adding Prodigi requires:
|
||||
1. `lib/simpleshop_theme/clients/prodigi.ex` — HTTP client
|
||||
2. `lib/simpleshop_theme/providers/prodigi.ex` — Provider behaviour implementation
|
||||
1. `lib/berrypod/clients/prodigi.ex` — HTTP client
|
||||
2. `lib/berrypod/providers/prodigi.ex` — Provider behaviour implementation
|
||||
3. Migration: update provider_connections to support "prodigi" type
|
||||
4. Admin UI: provider selection during setup
|
||||
|
||||
@@ -15,7 +15,7 @@ Product maps have: `.name`, `.category`, `.description`, `.slug`, `.price`, `.im
|
||||
## Changes
|
||||
|
||||
### 1. CartHook — add search assigns + event handler
|
||||
**File:** `lib/simpleshop_theme_web/cart_hook.ex`
|
||||
**File:** `lib/berrypod_web/cart_hook.ex`
|
||||
|
||||
- Init assigns in `on_mount`: `search_results: []`, `search_query: ""`
|
||||
- Handle `"search"` event (from `phx-keyup`):
|
||||
@@ -24,7 +24,7 @@ Product maps have: `.name`, `.category`, `.description`, `.slug`, `.price`, `.im
|
||||
- Handle `"close_search"` event → clear query + results + hide modal via JS
|
||||
|
||||
### 2. shop_layout + search_modal — add search attrs and UI
|
||||
**File:** `lib/simpleshop_theme_web/components/shop_components/layout.ex`
|
||||
**File:** `lib/berrypod_web/components/shop_components/layout.ex`
|
||||
|
||||
**shop_layout:**
|
||||
- Add optional attrs: `search_results` (default `[]`), `search_query` (default `""`)
|
||||
@@ -41,7 +41,7 @@ Product maps have: `.name`, `.category`, `.description`, `.slug`, `.price`, `.im
|
||||
- Click on result: navigate to product, close modal (JS.hide + close_search event)
|
||||
|
||||
### 3. Page templates — thread search assigns
|
||||
**All 8 files in** `lib/simpleshop_theme_web/components/page_templates/`
|
||||
**All 8 files in** `lib/berrypod_web/components/page_templates/`
|
||||
|
||||
Add two lines to each `<.shop_layout>` call:
|
||||
```
|
||||
@@ -52,9 +52,9 @@ search_query={assigns[:search_query] || ""}
|
||||
Same pattern as `cart_drawer_open` and `cart_status`.
|
||||
|
||||
## Files to modify
|
||||
1. `lib/simpleshop_theme_web/cart_hook.ex`
|
||||
2. `lib/simpleshop_theme_web/components/shop_components/layout.ex` (shop_layout + search_modal)
|
||||
3. All 8 page templates in `lib/simpleshop_theme_web/components/page_templates/`
|
||||
1. `lib/berrypod_web/cart_hook.ex`
|
||||
2. `lib/berrypod_web/components/shop_components/layout.ex` (shop_layout + search_modal)
|
||||
3. All 8 page templates in `lib/berrypod_web/components/page_templates/`
|
||||
|
||||
## Verification
|
||||
- Browser: open search modal on multiple pages, type queries, verify results appear and link correctly
|
||||
|
||||
@@ -28,7 +28,7 @@ This plan covers the first piece. The other two are separate tasks that build on
|
||||
|
||||
## Design: single-admin, closed registration
|
||||
|
||||
SimpleShop is single-tenant: one shop, one admin. The setup wizard replaces the generic registration flow entirely.
|
||||
Berrypod is single-tenant: one shop, one admin. The setup wizard replaces the generic registration flow entirely.
|
||||
|
||||
### Fresh install flow
|
||||
|
||||
@@ -62,28 +62,28 @@ The setup wizard checks three things before allowing "go live":
|
||||
|
||||
| Step | How to check | Module/function |
|
||||
|------|-------------|-----------------|
|
||||
| Printify connected | `Products.get_provider_connection_by_type("printify")` returns a connection with a non-nil `api_key_encrypted` | `SimpleshopTheme.Products` |
|
||||
| Products synced | `Products.count_products_for_connection(conn.id) > 0` | `SimpleshopTheme.Products` |
|
||||
| Stripe connected | `Settings.has_secret?("stripe_api_key")` | `SimpleshopTheme.Settings` |
|
||||
| Printify connected | `Products.get_provider_connection_by_type("printify")` returns a connection with a non-nil `api_key_encrypted` | `Berrypod.Products` |
|
||||
| Products synced | `Products.count_products_for_connection(conn.id) > 0` | `Berrypod.Products` |
|
||||
| Stripe connected | `Settings.has_secret?("stripe_api_key")` | `Berrypod.Settings` |
|
||||
|
||||
Optional (nice-to-have, not blocking go-live):
|
||||
- Stripe webhook configured: `Settings.has_secret?("stripe_webhook_signing_secret")`
|
||||
- Shop name customised: `theme_settings.site_name != "SimpleShop"` (or similar default check)
|
||||
- Shop name customised: `theme_settings.site_name != "Berrypod"` (or similar default check)
|
||||
|
||||
## Changes
|
||||
|
||||
### 1. `Accounts.has_admin?/0` and registration lockdown
|
||||
|
||||
**File:** `lib/simpleshop_theme/accounts.ex`
|
||||
**File:** `lib/berrypod/accounts.ex`
|
||||
|
||||
- Add `has_admin?/0` — `Repo.exists?(User)` (any user = admin exists)
|
||||
- This is the single check that gates registration
|
||||
|
||||
**File:** `lib/simpleshop_theme_web/live/user_live/registration.ex`
|
||||
**File:** `lib/berrypod_web/live/user_live/registration.ex`
|
||||
|
||||
- In `mount/3`, check `Accounts.has_admin?()` — if true, redirect to `/users/log-in` with a flash like "Registration is closed"
|
||||
|
||||
**File:** `lib/simpleshop_theme_web/router.ex`
|
||||
**File:** `lib/berrypod_web/router.ex`
|
||||
|
||||
- No route changes needed yet — the LiveView mount handles the redirect
|
||||
|
||||
@@ -91,14 +91,14 @@ Optional (nice-to-have, not blocking go-live):
|
||||
|
||||
### 2. Add `site_live` setting and setup status
|
||||
|
||||
**File:** `lib/simpleshop_theme/settings.ex`
|
||||
**File:** `lib/berrypod/settings.ex`
|
||||
|
||||
- Add `site_live?/0` — reads `get_setting("shop", "site_live")`, returns boolean (default `false`)
|
||||
- Add `set_site_live/1` — writes `put_setting("shop", "site_live", value)`
|
||||
|
||||
No migration needed — settings table already stores arbitrary key/value pairs.
|
||||
|
||||
**File:** `lib/simpleshop_theme/setup.ex` (new module)
|
||||
**File:** `lib/berrypod/setup.ex` (new module)
|
||||
|
||||
- `setup_status/0` returns a map:
|
||||
```elixir
|
||||
@@ -118,24 +118,24 @@ No migration needed — settings table already stores arbitrary key/value pairs.
|
||||
|
||||
### 3. Fresh install redirect (no admin exists)
|
||||
|
||||
**File:** `lib/simpleshop_theme_web/hooks/theme_hook.ex`
|
||||
**File:** `lib/berrypod_web/hooks/theme_hook.ex`
|
||||
|
||||
ThemeHook already runs on every public shop page mount. Add early check:
|
||||
|
||||
- If `Accounts.has_admin?()` is false → redirect to `/setup`
|
||||
- This catches the fresh install case before any other logic runs
|
||||
|
||||
**File:** `lib/simpleshop_theme_web/live/setup_live.ex` (new)
|
||||
**File:** `lib/berrypod_web/live/setup_live.ex` (new)
|
||||
|
||||
A simple public LiveView at `/setup` that:
|
||||
- If admin already exists → redirect to `/users/log-in`
|
||||
- If no admin → show "Welcome to SimpleShop" with email input form
|
||||
- If no admin → show "Welcome to Berrypod" with email input form
|
||||
- On submit → calls `Accounts.register_user/1` and sends magic link
|
||||
- Shows "Check your email" confirmation
|
||||
|
||||
This reuses the existing registration logic but with a different UI (setup-focused, not generic registration).
|
||||
|
||||
**File:** `lib/simpleshop_theme_web/router.ex`
|
||||
**File:** `lib/berrypod_web/router.ex`
|
||||
|
||||
- Add `/setup` route in a minimal live_session (no ThemeHook, no CartHook — avoids the redirect loop)
|
||||
|
||||
@@ -143,20 +143,20 @@ This reuses the existing registration logic but with a different UI (setup-focus
|
||||
|
||||
### 4. "Coming soon" page for public visitors
|
||||
|
||||
**File:** `lib/simpleshop_theme_web/hooks/theme_hook.ex`
|
||||
**File:** `lib/berrypod_web/hooks/theme_hook.ex`
|
||||
|
||||
Extend the ThemeHook logic (after the fresh install check):
|
||||
|
||||
- If `site_live?()` is false AND user is not authenticated → redirect to `/coming-soon`
|
||||
|
||||
**File:** `lib/simpleshop_theme_web/live/shop_live/coming_soon.ex` (new)
|
||||
**File:** `lib/berrypod_web/live/shop_live/coming_soon.ex` (new)
|
||||
|
||||
Minimal LiveView:
|
||||
- Uses the shop root layout (gets theme styling) but no nav/footer
|
||||
- Shows site name/logo, "Coming soon" heading, optional tagline
|
||||
- No redirect loop — this page itself doesn't trigger the gate
|
||||
|
||||
**File:** `lib/simpleshop_theme_web/router.ex`
|
||||
**File:** `lib/berrypod_web/router.ex`
|
||||
|
||||
- Add `/coming-soon` route in the public shop live_session but mark it as exempt from the gate (via assign or separate handling in ThemeHook)
|
||||
|
||||
@@ -164,7 +164,7 @@ Minimal LiveView:
|
||||
|
||||
### 5. Admin setup checklist page
|
||||
|
||||
**File:** `lib/simpleshop_theme_web/live/admin/setup_live.ex` (new)
|
||||
**File:** `lib/berrypod_web/live/admin/setup_live.ex` (new)
|
||||
|
||||
Admin page at `/admin/setup` showing:
|
||||
|
||||
@@ -177,7 +177,7 @@ Admin page at `/admin/setup` showing:
|
||||
Each step shows what to do and links to where to do it. Feels like guided onboarding, not a settings dump.
|
||||
|
||||
**Files:**
|
||||
- `lib/simpleshop_theme_web/live/admin/setup_live.ex`
|
||||
- `lib/berrypod_web/live/admin/setup_live.ex`
|
||||
- Router update to add the route
|
||||
- Admin nav update to include "Setup" link (prominent when not live)
|
||||
|
||||
@@ -185,7 +185,7 @@ Each step shows what to do and links to where to do it. Feels like guided onboar
|
||||
|
||||
### 6. Admin bar "not live" indicator
|
||||
|
||||
**File:** `lib/simpleshop_theme_web/components/shop_components/layout.ex`
|
||||
**File:** `lib/berrypod_web/components/shop_components/layout.ex`
|
||||
|
||||
- When shop is not live, show a banner in the admin bar: "Your shop is not live — [Complete setup →]"
|
||||
- When shop is live, the setup page becomes a less prominent settings link
|
||||
@@ -194,7 +194,7 @@ Each step shows what to do and links to where to do it. Feels like guided onboar
|
||||
|
||||
### 7. Post-login redirect for fresh admin
|
||||
|
||||
**File:** `lib/simpleshop_theme_web/user_auth.ex`
|
||||
**File:** `lib/berrypod_web/user_auth.ex`
|
||||
|
||||
- After confirming magic link (first login ever), redirect to `/admin/setup` instead of `/`
|
||||
- Subsequent logins go to `/` as normal (or `/admin/setup` if not live yet)
|
||||
|
||||
@@ -24,7 +24,7 @@ Fetch shipping rates from Printify during product sync, cache them in DB, displa
|
||||
|
||||
## 1. Migration + schema: `shipping_rates`
|
||||
|
||||
**New file:** `lib/simpleshop_theme/shipping/shipping_rate.ex`
|
||||
**New file:** `lib/berrypod/shipping/shipping_rate.ex`
|
||||
|
||||
```
|
||||
shipping_rates table:
|
||||
@@ -53,7 +53,7 @@ Update `Order` changeset to cast `:shipping_cost`. Update `total` calculation: `
|
||||
|
||||
## 3. Provider behaviour: add `fetch_shipping_rates/2`
|
||||
|
||||
**File:** `lib/simpleshop_theme/providers/provider.ex`
|
||||
**File:** `lib/berrypod/providers/provider.ex`
|
||||
|
||||
```elixir
|
||||
@callback fetch_shipping_rates(ProviderConnection.t(), products :: [map()]) ::
|
||||
@@ -66,7 +66,7 @@ Use `@optional_callbacks [fetch_shipping_rates: 2]` — the sync worker checks `
|
||||
|
||||
## 4. Printify implementation
|
||||
|
||||
**File:** `lib/simpleshop_theme/providers/printify.ex`
|
||||
**File:** `lib/berrypod/providers/printify.ex`
|
||||
|
||||
New function `fetch_shipping_rates/2`:
|
||||
|
||||
@@ -90,7 +90,7 @@ New function `fetch_shipping_rates/2`:
|
||||
|
||||
## 5. Shipping context
|
||||
|
||||
**New file:** `lib/simpleshop_theme/shipping.ex`
|
||||
**New file:** `lib/berrypod/shipping.ex`
|
||||
|
||||
Functions:
|
||||
- `upsert_rates(provider_connection_id, rates)` — bulk upsert via `Repo.insert_all` with `on_conflict: :replace`
|
||||
@@ -108,7 +108,7 @@ Functions:
|
||||
|
||||
## 6. Wire shipping into ProductSyncWorker
|
||||
|
||||
**File:** `lib/simpleshop_theme/sync/product_sync_worker.ex`
|
||||
**File:** `lib/berrypod/sync/product_sync_worker.ex`
|
||||
|
||||
In `do_sync_products/1`, after `sync_all_products(conn, products)` and before `update_sync_status(conn, "completed", ...)` (line 99), add:
|
||||
|
||||
@@ -120,7 +120,7 @@ Private function wraps `provider.fetch_shipping_rates(conn, products)` → `Ship
|
||||
|
||||
## 7. Scheduled sync worker
|
||||
|
||||
**New file:** `lib/simpleshop_theme/sync/scheduled_sync_worker.ex`
|
||||
**New file:** `lib/berrypod/sync/scheduled_sync_worker.ex`
|
||||
|
||||
```elixir
|
||||
use Oban.Worker, queue: :sync, max_attempts: 1
|
||||
@@ -130,14 +130,14 @@ Calls `Products.list_provider_connections()`, filters enabled, enqueues `Product
|
||||
|
||||
**Oban cron config** (`config/config.exs`):
|
||||
```elixir
|
||||
{"0 */6 * * *", SimpleshopTheme.Sync.ScheduledSyncWorker}
|
||||
{"0 */6 * * *", Berrypod.Sync.ScheduledSyncWorker}
|
||||
```
|
||||
|
||||
Every 6 hours. `:sync` queue concurrency 1 serialises with manual syncs.
|
||||
|
||||
## 8. Checkout: Stripe shipping_options
|
||||
|
||||
**File:** `lib/simpleshop_theme_web/controllers/checkout_controller.ex`
|
||||
**File:** `lib/berrypod_web/controllers/checkout_controller.ex`
|
||||
|
||||
Calculate shipping for GB (domestic) and US (international representative). Build `shipping_options` with inline `shipping_rate_data`:
|
||||
|
||||
@@ -156,13 +156,13 @@ shipping_options: [
|
||||
|
||||
If no rates found, omit `shipping_options` (current behaviour preserved).
|
||||
|
||||
**File:** `lib/simpleshop_theme_web/controllers/stripe_webhook_controller.ex`
|
||||
**File:** `lib/berrypod_web/controllers/stripe_webhook_controller.ex`
|
||||
|
||||
On `checkout.session.completed`, extract `session.shipping_cost.amount_total` and update order's `shipping_cost` and `total`.
|
||||
|
||||
## 9. Country detection plug
|
||||
|
||||
**New file:** `lib/simpleshop_theme_web/plugs/country_detect.ex`
|
||||
**New file:** `lib/berrypod_web/plugs/country_detect.ex`
|
||||
|
||||
Simple plug that runs on the `:browser` pipeline:
|
||||
1. Check session for existing `country_code` — if present, skip
|
||||
@@ -174,11 +174,11 @@ LiveViews read it from the session in `mount/3` via `get_session(session, "count
|
||||
|
||||
## 10. Cart display
|
||||
|
||||
**File:** `lib/simpleshop_theme_web/live/shop/cart.ex`
|
||||
**File:** `lib/berrypod_web/live/shop/cart.ex`
|
||||
|
||||
Read `country_code` from session (default `"GB"`). Add `shipping_estimate` assign on mount using `Shipping.calculate_for_cart(cart_items, country_code)`.
|
||||
|
||||
**File:** `lib/simpleshop_theme_web/components/shop_components/cart.ex`
|
||||
**File:** `lib/berrypod_web/components/shop_components/cart.ex`
|
||||
|
||||
Update `order_summary` component — add `attr :shipping_estimate, :integer, default: nil`:
|
||||
- Non-nil → show `"From #{format_price(shipping_estimate)}"` + total includes estimate
|
||||
@@ -194,23 +194,23 @@ Cart drawer unchanged (compact view, no shipping detail).
|
||||
|------|--------|
|
||||
| `priv/repo/migrations/..._create_shipping_rates.exs` | New migration |
|
||||
| `priv/repo/migrations/..._add_shipping_cost_to_orders.exs` | New migration |
|
||||
| `lib/simpleshop_theme/shipping/shipping_rate.ex` | New schema |
|
||||
| `lib/simpleshop_theme/shipping.ex` | New context |
|
||||
| `lib/simpleshop_theme/providers/provider.ex` | Add optional callback |
|
||||
| `lib/simpleshop_theme/providers/printify.ex` | Implement `fetch_shipping_rates/2` |
|
||||
| `lib/simpleshop_theme/sync/product_sync_worker.ex` | Wire shipping into sync |
|
||||
| `lib/simpleshop_theme/sync/scheduled_sync_worker.ex` | New Oban cron worker |
|
||||
| `lib/berrypod/shipping/shipping_rate.ex` | New schema |
|
||||
| `lib/berrypod/shipping.ex` | New context |
|
||||
| `lib/berrypod/providers/provider.ex` | Add optional callback |
|
||||
| `lib/berrypod/providers/printify.ex` | Implement `fetch_shipping_rates/2` |
|
||||
| `lib/berrypod/sync/product_sync_worker.ex` | Wire shipping into sync |
|
||||
| `lib/berrypod/sync/scheduled_sync_worker.ex` | New Oban cron worker |
|
||||
| `config/config.exs` | Add ScheduledSyncWorker to crontab |
|
||||
| `lib/simpleshop_theme/orders/order.ex` | Add `shipping_cost` field |
|
||||
| `lib/simpleshop_theme/orders.ex` | Cast shipping_cost, update total logic |
|
||||
| `lib/simpleshop_theme/products.ex` | Add `get_variants_with_products/1` |
|
||||
| `lib/simpleshop_theme_web/controllers/checkout_controller.ex` | Stripe shipping_options |
|
||||
| `lib/simpleshop_theme_web/controllers/stripe_webhook_controller.ex` | Extract shipping from Stripe |
|
||||
| `lib/simpleshop_theme_web/plugs/country_detect.ex` | New plug: country from Accept-Language |
|
||||
| `lib/simpleshop_theme_web/router.ex` | Add CountryDetect to `:browser` pipeline |
|
||||
| `lib/simpleshop_theme_web/live/shop/cart.ex` | Shipping estimate assign + country from session |
|
||||
| `lib/simpleshop_theme_web/components/shop_components/cart.ex` | Display estimate |
|
||||
| `lib/simpleshop_theme_web/components/page_templates/cart.html.heex` | Pass shipping_estimate |
|
||||
| `lib/berrypod/orders/order.ex` | Add `shipping_cost` field |
|
||||
| `lib/berrypod/orders.ex` | Cast shipping_cost, update total logic |
|
||||
| `lib/berrypod/products.ex` | Add `get_variants_with_products/1` |
|
||||
| `lib/berrypod_web/controllers/checkout_controller.ex` | Stripe shipping_options |
|
||||
| `lib/berrypod_web/controllers/stripe_webhook_controller.ex` | Extract shipping from Stripe |
|
||||
| `lib/berrypod_web/plugs/country_detect.ex` | New plug: country from Accept-Language |
|
||||
| `lib/berrypod_web/router.ex` | Add CountryDetect to `:browser` pipeline |
|
||||
| `lib/berrypod_web/live/shop/cart.ex` | Shipping estimate assign + country from session |
|
||||
| `lib/berrypod_web/components/shop_components/cart.ex` | Display estimate |
|
||||
| `lib/berrypod_web/components/page_templates/cart.html.heex` | Pass shipping_estimate |
|
||||
|
||||
**New files:** 5 (schema, context, worker, plug, 2 migrations)
|
||||
**Modified files:** 14
|
||||
|
||||
Reference in New Issue
Block a user