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:
jamey 2026-02-18 21:23:15 +00:00
parent c65e777832
commit 9528700862
300 changed files with 23932 additions and 1349 deletions

View File

@ -6,23 +6,23 @@
{"lib/mix/tasks/optimize_images.ex", :unknown_function}, {"lib/mix/tasks/optimize_images.ex", :unknown_function},
{"lib/mix/tasks/register_webhooks.ex", :callback_info_missing}, {"lib/mix/tasks/register_webhooks.ex", :callback_info_missing},
{"lib/mix/tasks/register_webhooks.ex", :unknown_function}, {"lib/mix/tasks/register_webhooks.ex", :unknown_function},
{"lib/mix/tasks/simpleshop/download_images.ex", :callback_info_missing}, {"lib/mix/tasks/berrypod/download_images.ex", :callback_info_missing},
{"lib/mix/tasks/simpleshop/download_images.ex", :unknown_function}, {"lib/mix/tasks/berrypod/download_images.ex", :unknown_function},
# Stripe library type specs cause false positives # Stripe library type specs cause false positives
{"lib/simpleshop_theme/stripe/setup.ex", :pattern_match_cov}, {"lib/berrypod/stripe/setup.ex", :pattern_match_cov},
{"lib/simpleshop_theme/stripe/setup.ex", :pattern_match}, {"lib/berrypod/stripe/setup.ex", :pattern_match},
{"lib/simpleshop_theme/stripe/setup.ex", :no_return}, {"lib/berrypod/stripe/setup.ex", :no_return},
{"lib/simpleshop_theme/stripe/setup.ex", :call}, {"lib/berrypod/stripe/setup.ex", :call},
{"lib/simpleshop_theme_web/controllers/checkout_controller.ex", :call}, {"lib/berrypod_web/controllers/checkout_controller.ex", :call},
{"lib/simpleshop_theme_web/controllers/checkout_controller.ex", :pattern_match_cov}, {"lib/berrypod_web/controllers/checkout_controller.ex", :pattern_match_cov},
{"lib/simpleshop_theme_web/controllers/checkout_controller.ex", :no_return}, {"lib/berrypod_web/controllers/checkout_controller.ex", :no_return},
# Environment-dependent: localhost?() is always true in dev # Environment-dependent: localhost?() is always true in dev
{"lib/simpleshop_theme_web/live/admin/settings.ex", :pattern_match}, {"lib/berrypod_web/live/admin/settings.ex", :pattern_match},
# Provider behaviour type not derived by dialyzer # Provider behaviour type not derived by dialyzer
{"lib/simpleshop_theme/providers/provider.ex", :unknown_type}, {"lib/berrypod/providers/provider.ex", :unknown_type},
# ExUnit internals not in PLT (test support files) # ExUnit internals not in PLT (test support files)
{"test/support/conn_case.ex", :unknown_function}, {"test/support/conn_case.ex", :unknown_function},

2
.gitignore vendored
View File

@ -23,7 +23,7 @@ erl_crash.dump
/tmp/ /tmp/
# Ignore package tarball (built via "mix hex.build"). # Ignore package tarball (built via "mix hex.build").
simpleshop_theme-*.tar berrypod-*.tar
# Ignore assets that are produced by build tools. # Ignore assets that are produced by build tools.
/priv/static/assets/ /priv/static/assets/

View File

@ -4,7 +4,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
## Project Overview ## Project Overview
SimpleShop is a customisable e-commerce storefront for print-on-demand sellers, built with Phoenix 1.8 + LiveView 1.1 on Elixir/Erlang. Uses SQLite with BLOB storage for images. Licensed under AGPL-3.0. Berrypod is a customisable e-commerce storefront for print-on-demand sellers, built with Phoenix 1.8 + LiveView 1.1 on Elixir/Erlang. Uses SQLite with BLOB storage for images. Licensed under AGPL-3.0.
## Common Commands ## Common Commands
@ -19,7 +19,7 @@ mix precommit # REQUIRED before committing: compile --warning-as-errors
## Architecture ## Architecture
### Core Contexts (lib/simpleshop_theme/) ### Core Contexts (lib/berrypod/)
- **Settings** - Theme configuration persistence as JSON - **Settings** - Theme configuration persistence as JSON
- **Theme** - CSS generation, ETS caching, 8 presets (Gallery, Studio, Boutique, etc.) - **Theme** - CSS generation, ETS caching, 8 presets (Gallery, Studio, Boutique, etc.)
@ -27,7 +27,7 @@ mix precommit # REQUIRED before committing: compile --warning-as-errors
- **Media/Images** - Image uploads and optimization pipeline (Oban jobs) - **Media/Images** - Image uploads and optimization pipeline (Oban jobs)
- **Providers** - Abstraction layer for POD providers (Printify integration) - **Providers** - Abstraction layer for POD providers (Printify integration)
### Web Layer (lib/simpleshop_theme_web/) ### Web Layer (lib/berrypod_web/)
- **live/** - LiveViews for shop pages and theme editor - **live/** - LiveViews for shop pages and theme editor
- **components/page_templates/** - Shared templates between preview and live shop - **components/page_templates/** - Shared templates between preview and live shop
@ -78,7 +78,7 @@ Theme switching is instant via CSS custom property injection (no reload).
Routes requiring auth go in `:require_authenticated_user` live_session: Routes requiring auth go in `:require_authenticated_user` live_session:
```elixir ```elixir
live_session :require_authenticated_user, live_session :require_authenticated_user,
on_mount: [{SimpleshopThemeWeb.UserAuth, :require_authenticated}] do on_mount: [{BerrypodWeb.UserAuth, :require_authenticated}] do
live "/admin/theme", ThemeLive.Index live "/admin/theme", ThemeLive.Index
end end
``` ```
@ -86,7 +86,7 @@ end
Public routes with optional user go in `:current_user` live_session: Public routes with optional user go in `:current_user` live_session:
```elixir ```elixir
live_session :current_user, live_session :current_user,
on_mount: [{SimpleshopThemeWeb.UserAuth, :mount_current_scope}] do on_mount: [{BerrypodWeb.UserAuth, :mount_current_scope}] do
live "/", ShopLive.Home live "/", ShopLive.Home
end end
``` ```

View File

@ -1,15 +1,15 @@
# Lean Alpine-based image for SimpleshopTheme. # Lean Alpine-based image for Berrypod.
# #
# Builder and runner both use Alpine (musl). The vix NIF is compiled against # Builder and runner both use Alpine (musl). The vix NIF is compiled against
# Alpine's system libvips instead of the glibc-linked precompiled binary. # Alpine's system libvips instead of the glibc-linked precompiled binary.
# #
# Build: docker build -t simpleshop_theme . # Build: docker build -t berrypod .
# Run: docker run --rm -p 4000:4000 \ # Run: docker run --rm -p 4000:4000 \
# -e SECRET_KEY_BASE=$(mix phx.gen.secret) \ # -e SECRET_KEY_BASE=$(mix phx.gen.secret) \
# -e DATABASE_PATH=/data/simpleshop_theme.db \ # -e DATABASE_PATH=/data/berrypod.db \
# -e PHX_HOST=localhost \ # -e PHX_HOST=localhost \
# -v simpleshop_data:/data \ # -v berrypod_data:/data \
# simpleshop_theme # berrypod
# #
# Images: # Images:
# https://hub.docker.com/r/hexpm/elixir/tags?name=alpine # https://hub.docker.com/r/hexpm/elixir/tags?name=alpine
@ -101,12 +101,12 @@ RUN mkdir -p /data && chown appuser:appuser /data
ENV MIX_ENV="prod" ENV MIX_ENV="prod"
# Copy the release from the builder # Copy the release from the builder
COPY --from=builder --chown=appuser:appuser /app/_build/${MIX_ENV}/rel/simpleshop_theme ./ COPY --from=builder --chown=appuser:appuser /app/_build/${MIX_ENV}/rel/berrypod ./
USER appuser USER appuser
# Ensure /data is the default database location # Ensure /data is the default database location
ENV DATABASE_PATH="/data/simpleshop_theme.db" ENV DATABASE_PATH="/data/berrypod.db"
EXPOSE 4000 EXPOSE 4000

View File

@ -1,4 +1,4 @@
# SimpleShop Progress # Berrypod Progress
> Single source of truth for project status and task tracking. > Single source of truth for project status and task tracking.
@ -110,7 +110,7 @@ Issues from hands-on testing of the deployed prod site (Feb 2025). 16 of 18 comp
### Tier 5 — Platform vision ### Tier 5 — Platform vision
16. **Hosted platform** — Marketing/brochure site for SimpleShop as a service. Subscribe/sign-up flow. Multi-tenancy with per-tenant databases. Stripe Connect for customer shops (each merchant connects their own Stripe account via OAuth). 16. **Hosted platform** — Marketing/brochure site for Berrypod as a service. Subscribe/sign-up flow. Multi-tenancy with per-tenant databases. Stripe Connect for customer shops (each merchant connects their own Stripe account via OAuth).
17. **Migration & export** — Let shop owners export their data (products, orders, customers, theme settings). Import from other platforms (Shopify, WooCommerce). Portable data as a selling point for the self-hosted story. 17. **Migration & export** — Let shop owners export their data (products, orders, customers, theme settings). Import from other platforms (Shopify, WooCommerce). Portable data as a selling point for the self-hosted story.
18. **Internationalisation (i18n)** — Multi-language support via Gettext (already in Phoenix). Currency formatting. RTL layout support. Per-shop locale configuration. **Note:** `ex_money`/`ex_cldr` are currently used *only* for `Cart.format_price/1` (a single GBP formatting call) but add ~13 MB to the release (ex_cldr 9.5 MB, digital_token 3.7 MB, ex_cldr_numbers, ex_cldr_currencies). Consider replacing with a simple `format_price/2` function that handles GBP/EUR/USD directly — all three use 2 decimal places and are trivial to format. Re-add `ex_money` later if proper locale-aware number formatting is needed (e.g., German `12.345,67 €`). 18. **Internationalisation (i18n)** — Multi-language support via Gettext (already in Phoenix). Currency formatting. RTL layout support. Per-shop locale configuration. **Note:** `ex_money`/`ex_cldr` are currently used *only* for `Cart.format_price/1` (a single GBP formatting call) but add ~13 MB to the release (ex_cldr 9.5 MB, digital_token 3.7 MB, ex_cldr_numbers, ex_cldr_currencies). Consider replacing with a simple `format_price/2` function that handles GBP/EUR/USD directly — all three use 2 decimal places and are trivial to format. Re-add `ex_money` later if proper locale-aware number formatting is needed (e.g., German `12.345,67 €`).
@ -141,7 +141,7 @@ Issues from hands-on testing of the deployed prod site (Feb 2025). 16 of 18 comp
- ImageDownloadWorker downloads and links images to ProductImage - ImageDownloadWorker downloads and links images to ProductImage
- PreviewData uses local images for responsive `<picture>` elements - PreviewData uses local images for responsive `<picture>` elements
- Startup recovery re-enqueues pending downloads - Startup recovery re-enqueues pending downloads
- `mix simpleshop.download_images` backfill task - `mix berrypod.download_images` backfill task
See: [docs/plans/image-optimization.md](docs/plans/image-optimization.md) for implementation details See: [docs/plans/image-optimization.md](docs/plans/image-optimization.md) for implementation details
@ -160,7 +160,7 @@ See: [docs/plans/image-optimization.md](docs/plans/image-optimization.md) for im
- [x] Product image download pipeline (1b49b47) - [x] Product image download pipeline (1b49b47)
- Downloads Printify CDN images via ImageDownloadWorker - Downloads Printify CDN images via ImageDownloadWorker
- Processes through Media pipeline (WebP conversion, AVIF/WebP variants) - Processes through Media pipeline (WebP conversion, AVIF/WebP variants)
- Startup recovery and `mix simpleshop.download_images` backfill - Startup recovery and `mix berrypod.download_images` backfill
- [x] Variant selector component (880e7a2) - [x] Variant selector component (880e7a2)
- Color swatches with hex colors, size buttons - Color swatches with hex colors, size buttons
- Fixed Printify options parsing (Color/Size swap bug) - Fixed Printify options parsing (Color/Size swap bug)

View File

@ -1,4 +1,4 @@
# SimpleShop # Berrypod
A beautiful, customisable e-commerce storefront built with Phoenix LiveView. Designed for print-on-demand sellers who want professional shops without design expertise. A beautiful, customisable e-commerce storefront built with Phoenix LiveView. Designed for print-on-demand sellers who want professional shops without design expertise.
@ -49,7 +49,7 @@ Customise your shop's look without touching code:
```bash ```bash
# Clone the repository # Clone the repository
git clone <repo-url> git clone <repo-url>
cd simpleshop_theme cd berrypod
# Install dependencies # Install dependencies
mix setup mix setup
@ -72,7 +72,7 @@ mix test
``` ```
lib/ lib/
├── simpleshop_theme/ # Core business logic ├── berrypod/ # Core business logic
│ ├── settings.ex # Theme settings context │ ├── settings.ex # Theme settings context
│ ├── settings/ │ ├── settings/
│ │ └── theme_settings.ex # Theme settings schema │ │ └── theme_settings.ex # Theme settings schema
@ -83,7 +83,7 @@ lib/
│ ├── presets.ex # 8 theme presets │ ├── presets.ex # 8 theme presets
│ └── preview_data.ex # Mock data for previews │ └── preview_data.ex # Mock data for previews
├── simpleshop_theme_web/ # Web layer ├── berrypod_web/ # Web layer
│ ├── components/ │ ├── components/
│ │ ├── layouts/ # App and shop layouts │ │ ├── layouts/ # App and shop layouts
│ │ ├── page_templates/ # Shared page templates (*.heex) │ │ ├── page_templates/ # Shared page templates (*.heex)
@ -122,7 +122,7 @@ assets/css/
## Stripe Checkout ## Stripe Checkout
SimpleShop uses [Stripe Checkout](https://stripe.com/docs/payments/checkout) (hosted payment page) for secure payment processing. Berrypod uses [Stripe Checkout](https://stripe.com/docs/payments/checkout) (hosted payment page) for secure payment processing.
### Setup ### Setup
@ -181,7 +181,7 @@ mix generate_mockups --search "poster"
mix generate_mockups --list-blueprints mix generate_mockups --list-blueprints
``` ```
Product definitions are in `lib/simpleshop_theme/printify/mockup_generator.ex`. Product definitions are in `lib/berrypod/printify/mockup_generator.ex`.
## Documentation ## Documentation

View File

@ -1,4 +1,4 @@
# SimpleShop Roadmap # Berrypod Roadmap
> Vision and future features. For current status, see [PROGRESS.md](PROGRESS.md). > Vision and future features. For current status, see [PROGRESS.md](PROGRESS.md).
@ -75,4 +75,4 @@ Areas needing better coverage:
- Network error handling in LiveView - Network error handling in LiveView
### Rename Project ### Rename Project
The project is named `simpleshop_theme` but it's now a full storefront. Consider renaming to `simple_shop`. The project is named `berrypod` but it's now a full storefront. Consider renaming to `simple_shop`.

View File

@ -22,7 +22,7 @@ import "phoenix_html"
// Establish Phoenix Socket and LiveView configuration. // Establish Phoenix Socket and LiveView configuration.
import {Socket} from "phoenix" import {Socket} from "phoenix"
import {LiveSocket} from "phoenix_live_view" import {LiveSocket} from "phoenix_live_view"
import {hooks as colocatedHooks} from "phoenix-colocated/simpleshop_theme" import {hooks as colocatedHooks} from "phoenix-colocated/berrypod"
import topbar from "../vendor/topbar" import topbar from "../vendor/topbar"
// Hook to sync color picker and text input // Hook to sync color picker and text input

View File

@ -7,33 +7,33 @@
# General application configuration # General application configuration
import Config import Config
config :simpleshop_theme, :scopes, config :berrypod, :scopes,
user: [ user: [
default: true, default: true,
module: SimpleshopTheme.Accounts.Scope, module: Berrypod.Accounts.Scope,
assign_key: :current_scope, assign_key: :current_scope,
access_path: [:user, :id], access_path: [:user, :id],
schema_key: :user_id, schema_key: :user_id,
schema_type: :binary_id, schema_type: :binary_id,
schema_table: :users, schema_table: :users,
test_data_fixture: SimpleshopTheme.AccountsFixtures, test_data_fixture: Berrypod.AccountsFixtures,
test_setup_helper: :register_and_log_in_user test_setup_helper: :register_and_log_in_user
] ]
config :simpleshop_theme, config :berrypod,
env: config_env(), env: config_env(),
ecto_repos: [SimpleshopTheme.Repo], ecto_repos: [Berrypod.Repo],
generators: [timestamp_type: :utc_datetime, binary_id: true] generators: [timestamp_type: :utc_datetime, binary_id: true]
# Configures the endpoint # Configures the endpoint
config :simpleshop_theme, SimpleshopThemeWeb.Endpoint, config :berrypod, BerrypodWeb.Endpoint,
url: [host: "localhost"], url: [host: "localhost"],
adapter: Bandit.PhoenixAdapter, adapter: Bandit.PhoenixAdapter,
render_errors: [ render_errors: [
formats: [html: SimpleshopThemeWeb.ErrorHTML, json: SimpleshopThemeWeb.ErrorJSON], formats: [html: BerrypodWeb.ErrorHTML, json: BerrypodWeb.ErrorJSON],
layout: false layout: false
], ],
pubsub_server: SimpleshopTheme.PubSub, pubsub_server: Berrypod.PubSub,
live_view: [signing_salt: "HWdz7Vt8"] live_view: [signing_salt: "HWdz7Vt8"]
# Configures the mailer # Configures the mailer
@ -43,22 +43,22 @@ config :simpleshop_theme, SimpleshopThemeWeb.Endpoint,
# #
# For production it's recommended to configure a different adapter # For production it's recommended to configure a different adapter
# at the `config/runtime.exs`. # at the `config/runtime.exs`.
config :simpleshop_theme, SimpleshopTheme.Mailer, adapter: Swoosh.Adapters.Local config :berrypod, Berrypod.Mailer, adapter: Swoosh.Adapters.Local
# Configure esbuild (the version is required) # Configure esbuild (the version is required)
config :esbuild, config :esbuild,
version: "0.25.4", version: "0.25.4",
simpleshop_theme: [ berrypod: [
args: args:
~w(js/app.js --bundle --target=es2022 --outdir=../priv/static/assets/js --external:/fonts/* --external:/images/* --alias:@=.), ~w(js/app.js --bundle --target=es2022 --outdir=../priv/static/assets/js --external:/fonts/* --external:/images/* --alias:@=.),
cd: Path.expand("../assets", __DIR__), cd: Path.expand("../assets", __DIR__),
env: %{"NODE_PATH" => [Path.expand("../deps", __DIR__), Mix.Project.build_path()]} env: %{"NODE_PATH" => [Path.expand("../deps", __DIR__), Mix.Project.build_path()]}
], ],
simpleshop_theme_shop_css: [ berrypod_shop_css: [
args: ~w(css/shop.css --bundle --outdir=../priv/static/assets/css), args: ~w(css/shop.css --bundle --outdir=../priv/static/assets/css),
cd: Path.expand("../assets", __DIR__) cd: Path.expand("../assets", __DIR__)
], ],
simpleshop_theme_admin_css: [ berrypod_admin_css: [
args: ~w(css/admin.css --bundle --outdir=../priv/static/assets/css), args: ~w(css/admin.css --bundle --outdir=../priv/static/assets/css),
cd: Path.expand("../assets", __DIR__) cd: Path.expand("../assets", __DIR__)
] ]
@ -72,28 +72,28 @@ config :logger, :default_formatter,
config :phoenix, :json_library, Jason config :phoenix, :json_library, Jason
# ex_money configuration for currency handling # ex_money configuration for currency handling
config :ex_money, default_cldr_backend: SimpleshopTheme.Cldr config :ex_money, default_cldr_backend: Berrypod.Cldr
# Error tracking (stored in DB, auto-captures Phoenix/LiveView/Oban errors) # Error tracking (stored in DB, auto-captures Phoenix/LiveView/Oban errors)
config :error_tracker, config :error_tracker,
repo: SimpleshopTheme.Repo, repo: Berrypod.Repo,
otp_app: :simpleshop_theme, otp_app: :berrypod,
plugins: [ErrorTracker.Plugins.Pruner] plugins: [ErrorTracker.Plugins.Pruner]
# Stripe configuration # Stripe configuration
config :stripity_stripe, api_version: "2024-12-18.acacia" config :stripity_stripe, api_version: "2024-12-18.acacia"
# Oban configuration for background jobs # Oban configuration for background jobs
config :simpleshop_theme, Oban, config :berrypod, Oban,
engine: Oban.Engines.Lite, engine: Oban.Engines.Lite,
repo: SimpleshopTheme.Repo, repo: Berrypod.Repo,
plugins: [ plugins: [
{Oban.Plugins.Pruner, max_age: 60}, {Oban.Plugins.Pruner, max_age: 60},
{Oban.Plugins.Lifeline, rescue_after: :timer.minutes(5)}, {Oban.Plugins.Lifeline, rescue_after: :timer.minutes(5)},
{Oban.Plugins.Cron, {Oban.Plugins.Cron,
crontab: [ crontab: [
{"*/30 * * * *", SimpleshopTheme.Orders.FulfilmentStatusWorker}, {"*/30 * * * *", Berrypod.Orders.FulfilmentStatusWorker},
{"0 */6 * * *", SimpleshopTheme.Sync.ScheduledSyncWorker} {"0 */6 * * *", Berrypod.Sync.ScheduledSyncWorker}
]} ]}
], ],
queues: [images: 2, sync: 1, checkout: 1] queues: [images: 2, sync: 1, checkout: 1]

View File

@ -1,8 +1,8 @@
import Config import Config
# Configure your database # Configure your database
config :simpleshop_theme, SimpleshopTheme.Repo, config :berrypod, Berrypod.Repo,
database: Path.expand("../simpleshop_theme_dev.db", __DIR__), database: Path.expand("../berrypod_dev.db", __DIR__),
pool_size: 5, pool_size: 5,
journal_mode: :wal, journal_mode: :wal,
busy_timeout: 5000, busy_timeout: 5000,
@ -15,7 +15,7 @@ config :simpleshop_theme, SimpleshopTheme.Repo,
# The watchers configuration can be used to run external # The watchers configuration can be used to run external
# watchers to your application. For example, we can use it # watchers to your application. For example, we can use it
# to bundle .js and .css sources. # to bundle .js and .css sources.
config :simpleshop_theme, SimpleshopThemeWeb.Endpoint, config :berrypod, BerrypodWeb.Endpoint,
# Binding to loopback ipv4 address prevents access from other machines. # Binding to loopback ipv4 address prevents access from other machines.
# Change to `ip: {0, 0, 0, 0}` to allow access from other machines. # Change to `ip: {0, 0, 0, 0}` to allow access from other machines.
http: [ip: {0, 0, 0, 0}, port: String.to_integer(System.get_env("PORT") || "4000")], http: [ip: {0, 0, 0, 0}, port: String.to_integer(System.get_env("PORT") || "4000")],
@ -24,11 +24,11 @@ config :simpleshop_theme, SimpleshopThemeWeb.Endpoint,
debug_errors: true, debug_errors: true,
secret_key_base: "Jk04sYT/pzfZ0cywS+i0vCURPoQYgqAGa72uS8bv2gydLyusWFc08kJyEnQP4zgT", secret_key_base: "Jk04sYT/pzfZ0cywS+i0vCURPoQYgqAGa72uS8bv2gydLyusWFc08kJyEnQP4zgT",
watchers: [ watchers: [
esbuild: {Esbuild, :install_and_run, [:simpleshop_theme, ~w(--sourcemap=inline --watch)]}, esbuild: {Esbuild, :install_and_run, [:berrypod, ~w(--sourcemap=inline --watch)]},
esbuild_shop_css: esbuild_shop_css:
{Esbuild, :install_and_run, [:simpleshop_theme_shop_css, ~w(--sourcemap=inline --watch)]}, {Esbuild, :install_and_run, [:berrypod_shop_css, ~w(--sourcemap=inline --watch)]},
esbuild_admin_css: esbuild_admin_css:
{Esbuild, :install_and_run, [:simpleshop_theme_admin_css, ~w(--sourcemap=inline --watch)]} {Esbuild, :install_and_run, [:berrypod_admin_css, ~w(--sourcemap=inline --watch)]}
] ]
# ## SSL Support # ## SSL Support
@ -55,18 +55,18 @@ config :simpleshop_theme, SimpleshopThemeWeb.Endpoint,
# different ports. # different ports.
# Watch static and templates for browser reloading. # Watch static and templates for browser reloading.
config :simpleshop_theme, SimpleshopThemeWeb.Endpoint, config :berrypod, BerrypodWeb.Endpoint,
live_reload: [ live_reload: [
web_console_logger: true, web_console_logger: true,
patterns: [ patterns: [
~r"priv/static/(?!uploads/).*(js|css|png|jpeg|jpg|gif|svg)$", ~r"priv/static/(?!uploads/).*(js|css|png|jpeg|jpg|gif|svg)$",
~r"priv/gettext/.*(po)$", ~r"priv/gettext/.*(po)$",
~r"lib/simpleshop_theme_web/(?:controllers|live|components|router)/?.*\.(ex|heex)$" ~r"lib/berrypod_web/(?:controllers|live|components|router)/?.*\.(ex|heex)$"
] ]
] ]
# Enable dev routes for dashboard and mailbox # Enable dev routes for dashboard and mailbox
config :simpleshop_theme, dev_routes: true config :berrypod, dev_routes: true
# Do not include metadata nor timestamps in development logs # Do not include metadata nor timestamps in development logs
config :logger, :default_formatter, format: "[$level] $message\n" config :logger, :default_formatter, format: "[$level] $message\n"

View File

@ -5,8 +5,7 @@ import Config
# manifest is generated by the `mix assets.deploy` task, # manifest is generated by the `mix assets.deploy` task,
# which you should run after static files are built and # which you should run after static files are built and
# before starting your production server. # before starting your production server.
config :simpleshop_theme, SimpleshopThemeWeb.Endpoint, config :berrypod, BerrypodWeb.Endpoint, cache_static_manifest: "priv/static/cache_manifest.json"
cache_static_manifest: "priv/static/cache_manifest.json"
# Configures Swoosh API Client # Configures Swoosh API Client
config :swoosh, api_client: Swoosh.ApiClient.Req config :swoosh, api_client: Swoosh.ApiClient.Req

View File

@ -12,12 +12,12 @@ import Config
# If you use `mix release`, you need to explicitly enable the server # If you use `mix release`, you need to explicitly enable the server
# by passing the PHX_SERVER=true when you start it: # by passing the PHX_SERVER=true when you start it:
# #
# PHX_SERVER=true bin/simpleshop_theme start # PHX_SERVER=true bin/berrypod start
# #
# Alternatively, you can use `mix phx.gen.release` to generate a `bin/server` # Alternatively, you can use `mix phx.gen.release` to generate a `bin/server`
# script that automatically sets the env var above. # script that automatically sets the env var above.
if System.get_env("PHX_SERVER") do if System.get_env("PHX_SERVER") do
config :simpleshop_theme, SimpleshopThemeWeb.Endpoint, server: true config :berrypod, BerrypodWeb.Endpoint, server: true
end end
if config_env() == :prod do if config_env() == :prod do
@ -25,10 +25,10 @@ if config_env() == :prod do
System.get_env("DATABASE_PATH") || System.get_env("DATABASE_PATH") ||
raise """ raise """
environment variable DATABASE_PATH is missing. environment variable DATABASE_PATH is missing.
For example: /data/simpleshop_theme.db For example: /data/berrypod.db
""" """
config :simpleshop_theme, SimpleshopTheme.Repo, config :berrypod, Berrypod.Repo,
database: database_path, database: database_path,
pool_size: String.to_integer(System.get_env("POOL_SIZE") || "5"), pool_size: String.to_integer(System.get_env("POOL_SIZE") || "5"),
journal_mode: :wal, journal_mode: :wal,
@ -49,9 +49,9 @@ if config_env() == :prod do
host = System.get_env("PHX_HOST") || "example.com" host = System.get_env("PHX_HOST") || "example.com"
port = String.to_integer(System.get_env("PORT") || "4000") port = String.to_integer(System.get_env("PORT") || "4000")
config :simpleshop_theme, :dns_cluster_query, System.get_env("DNS_CLUSTER_QUERY") config :berrypod, :dns_cluster_query, System.get_env("DNS_CLUSTER_QUERY")
config :simpleshop_theme, SimpleshopThemeWeb.Endpoint, config :berrypod, BerrypodWeb.Endpoint,
url: [host: host, port: 443, scheme: "https"], url: [host: host, port: 443, scheme: "https"],
http: [ http: [
# Enable IPv6 and bind on all interfaces. # Enable IPv6 and bind on all interfaces.
@ -68,7 +68,7 @@ if config_env() == :prod do
# To get SSL working, you will need to add the `https` key # To get SSL working, you will need to add the `https` key
# to your endpoint configuration: # to your endpoint configuration:
# #
# config :simpleshop_theme, SimpleshopThemeWeb.Endpoint, # config :berrypod, BerrypodWeb.Endpoint,
# https: [ # https: [
# ..., # ...,
# port: 443, # port: 443,
@ -90,7 +90,7 @@ if config_env() == :prod do
# We also recommend setting `force_ssl` in your config/prod.exs, # We also recommend setting `force_ssl` in your config/prod.exs,
# ensuring no data is ever sent via http, always redirecting to https: # ensuring no data is ever sent via http, always redirecting to https:
# #
# config :simpleshop_theme, SimpleshopThemeWeb.Endpoint, # config :berrypod, BerrypodWeb.Endpoint,
# force_ssl: [hsts: true] # force_ssl: [hsts: true]
# #
# Check `Plug.SSL` for all available options in `force_ssl`. # Check `Plug.SSL` for all available options in `force_ssl`.
@ -102,20 +102,20 @@ if config_env() == :prod do
# #
# Postmark (recommended): # Postmark (recommended):
# #
# config :simpleshop_theme, SimpleshopTheme.Mailer, # config :berrypod, Berrypod.Mailer,
# adapter: Swoosh.Adapters.Postmark, # adapter: Swoosh.Adapters.Postmark,
# api_key: System.get_env("POSTMARK_API_KEY") # api_key: System.get_env("POSTMARK_API_KEY")
# #
# Mailgun: # Mailgun:
# #
# config :simpleshop_theme, SimpleshopTheme.Mailer, # config :berrypod, Berrypod.Mailer,
# adapter: Swoosh.Adapters.Mailgun, # adapter: Swoosh.Adapters.Mailgun,
# api_key: System.get_env("MAILGUN_API_KEY"), # api_key: System.get_env("MAILGUN_API_KEY"),
# domain: System.get_env("MAILGUN_DOMAIN") # domain: System.get_env("MAILGUN_DOMAIN")
# #
# SMTP (any provider): # SMTP (any provider):
# #
# config :simpleshop_theme, SimpleshopTheme.Mailer, # config :berrypod, Berrypod.Mailer,
# adapter: Swoosh.Adapters.SMTP, # adapter: Swoosh.Adapters.SMTP,
# relay: System.get_env("SMTP_HOST"), # relay: System.get_env("SMTP_HOST"),
# port: String.to_integer(System.get_env("SMTP_PORT") || "587"), # port: String.to_integer(System.get_env("SMTP_PORT") || "587"),
@ -124,5 +124,5 @@ if config_env() == :prod do
# tls: :if_available # tls: :if_available
# Stripe and Printify keys are stored encrypted in the database and loaded # Stripe and Printify keys are stored encrypted in the database and loaded
# at runtime by SimpleshopTheme.Secrets. No env vars needed for those. # at runtime by Berrypod.Secrets. No env vars needed for those.
end end

View File

@ -8,8 +8,8 @@ config :bcrypt_elixir, :log_rounds, 1
# The MIX_TEST_PARTITION environment variable can be used # The MIX_TEST_PARTITION environment variable can be used
# to provide built-in test partitioning in CI environment. # to provide built-in test partitioning in CI environment.
# Run `mix help test` for more information. # Run `mix help test` for more information.
config :simpleshop_theme, SimpleshopTheme.Repo, config :berrypod, Berrypod.Repo,
database: Path.expand("../simpleshop_theme_test.db", __DIR__), database: Path.expand("../berrypod_test.db", __DIR__),
pool_size: 1, pool_size: 1,
pool: Ecto.Adapters.SQL.Sandbox, pool: Ecto.Adapters.SQL.Sandbox,
journal_mode: :wal, journal_mode: :wal,
@ -17,13 +17,13 @@ config :simpleshop_theme, SimpleshopTheme.Repo,
# We don't run a server during test. If one is required, # We don't run a server during test. If one is required,
# you can enable the server option below. # you can enable the server option below.
config :simpleshop_theme, SimpleshopThemeWeb.Endpoint, config :berrypod, BerrypodWeb.Endpoint,
http: [ip: {127, 0, 0, 1}, port: 4002], http: [ip: {127, 0, 0, 1}, port: 4002],
secret_key_base: "FgIOvs+0ZwA+GU1gInqNuyDz3zSnfeRu0kD7DYyGpC05/wE+G2xVJ5zlc/ufGedN", secret_key_base: "FgIOvs+0ZwA+GU1gInqNuyDz3zSnfeRu0kD7DYyGpC05/wE+G2xVJ5zlc/ufGedN",
server: false server: false
# In test we don't send emails # In test we don't send emails
config :simpleshop_theme, SimpleshopTheme.Mailer, adapter: Swoosh.Adapters.Test config :berrypod, Berrypod.Mailer, adapter: Swoosh.Adapters.Test
# Disable swoosh api client as it is only required for production adapters # Disable swoosh api client as it is only required for production adapters
config :swoosh, :api_client, false config :swoosh, :api_client, false
@ -39,7 +39,7 @@ config :phoenix_live_view,
enable_expensive_runtime_checks: true enable_expensive_runtime_checks: true
# Use inline testing mode for Oban # Use inline testing mode for Oban
config :simpleshop_theme, Oban, testing: :inline config :berrypod, Oban, testing: :inline
# Isolate image cache so test cleanup doesn't wipe the dev cache # Isolate image cache so test cleanup doesn't wipe the dev cache
config :simpleshop_theme, :image_cache_dir, Path.expand("../tmp/test_image_cache", __DIR__) config :berrypod, :image_cache_dir, Path.expand("../tmp/test_image_cache", __DIR__)

View File

@ -113,7 +113,7 @@ Build the structural pieces first, using existing DaisyUI components. No CSS cha
### 1.1 Admin shell layout ### 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: A shared admin layout component that wraps all admin pages:
@ -156,7 +156,7 @@ The shell provides:
### 1.2 Admin root layout ### 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: A dedicated root layout for admin pages that:
- Loads `app.css` (or later `app-admin.css`) - 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 - Doesn't include the shop nav/footer chrome
- Replaces the current generic `root.html.heex` for admin routes - 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`): The admin child layout (equivalent of `shop.html.heex`):
- Renders flash messages - Renders flash messages
@ -176,7 +176,7 @@ The admin child layout (equivalent of `shop.html.heex`):
### 1.3 Dashboard page ### 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: 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: 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: Sections:
- **Payments** — Stripe API key, webhook config (current `/admin/settings` content) - **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 ### 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: 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); @import "tailwindcss" source(none);
@source "../css"; @source "../css";
@source "../js"; @source "../js";
@source "../../lib/simpleshop_theme_web/live/admin"; @source "../../lib/berrypod_web/live/admin";
@source "../../lib/simpleshop_theme_web/live/user"; @source "../../lib/berrypod_web/live/user";
@source "../../lib/simpleshop_theme_web/components/admin_components.ex"; @source "../../lib/berrypod_web/components/admin_components.ex";
@source "../../lib/simpleshop_theme_web/components/core_components.ex"; @source "../../lib/berrypod_web/components/core_components.ex";
@source "../../lib/simpleshop_theme_web/components/layouts/admin_root.html.heex"; @source "../../lib/berrypod_web/components/layouts/admin_root.html.heex";
@source "../../lib/simpleshop_theme_web/components/layouts/admin.html.heex"; @source "../../lib/berrypod_web/components/layouts/admin.html.heex";
@plugin "../vendor/heroicons"; @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 ### 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: Replace DaisyUI class references with the new admin classes. This is a find-and-replace job:
- `btn btn-primary``admin-btn admin-btn-primary` - `btn btn-primary``admin-btn admin-btn-primary`

View File

@ -202,7 +202,7 @@ Acceptance: layout primitives available, no visual changes, all tests pass.
- Visual regression: PDP page - Visual regression: PDP page
Files modified: 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` - `assets/css/shop/components.css`
Acceptance: `product.ex` has zero inline styles, visual regression clean. 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 - Visual regression: cart page, cart drawer
Files modified: Files modified:
- `lib/simpleshop_theme_web/components/shop_components/layout.ex` (59 -> 0) - `lib/berrypod_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/cart.ex` (51 -> 0)
- `assets/css/shop/components.css` - `assets/css/shop/components.css`
Acceptance: both files zero inline styles, visual regression clean. 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) - Also `base.ex` (2)
Files modified: Files modified:
- `lib/simpleshop_theme_web/components/shop_components/content.ex` (57 -> 0) - `lib/berrypod_web/components/shop_components/content.ex` (57 -> 0)
- `lib/simpleshop_theme_web/components/shop_components/base.ex` (2 -> 0) - `lib/berrypod_web/components/shop_components/base.ex` (2 -> 0)
- `lib/simpleshop_theme_web/components/page_templates/*.html.heex` (29 -> 0) - `lib/berrypod_web/components/page_templates/*.html.heex` (29 -> 0)
- `assets/css/shop/components.css` - `assets/css/shop/components.css`
Acceptance: **zero inline styles remain** (0/281), full visual regression clean. 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): **5b** — Remove Tailwind shop build (~1.5h):
- Replace remaining Tailwind classes in `.heex` page templates - Replace remaining Tailwind classes in `.heex` page templates
- Remove `@import "tailwindcss"` from `app-shop.css` - 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` - Remove `tailwind_shop` watcher from `config/dev.exs`
- Update `assets.build` and `assets.deploy` Mix aliases - Update `assets.build` and `assets.deploy` Mix aliases
- Full visual regression - Full visual regression
@ -301,7 +301,7 @@ Acceptance: no Tailwind classes in shop code, Tailwind shop build removed, admin
Files modified: Files modified:
- `assets/css/admin-components.css` (new) - `assets/css/admin-components.css` (new)
- `assets/css/app.css` (remove DaisyUI) - `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 - All admin LiveView files
- Auth LiveView files - Auth LiveView files

View File

@ -133,7 +133,7 @@ end
**Net saving:** ~65 lines removed (80 duplicated minus ~15 for the helper function). **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. **Complexity:** Low. Pure refactor within one file.

View File

@ -126,9 +126,9 @@ For a production-quality system, Oban's durability is worth the small complexity
```elixir ```elixir
# config/config.exs # config/config.exs
config :simpleshop_theme, Oban, config :berrypod, Oban,
engine: Oban.Engines.Lite, # SQLite support engine: Oban.Engines.Lite, # SQLite support
repo: SimpleshopTheme.Repo, repo: Berrypod.Repo,
plugins: [ plugins: [
# Prune completed jobs after 60 seconds - keeps DB lean # Prune completed jobs after 60 seconds - keeps DB lean
{Oban.Plugins.Pruner, max_age: 60} {Oban.Plugins.Pruner, max_age: 60}
@ -144,7 +144,7 @@ With `max_age: 60`, the Oban tables typically contain:
### Job Worker ### Job Worker
```elixir ```elixir
defmodule SimpleshopTheme.Workers.ImageVariants do defmodule Berrypod.Workers.ImageVariants do
use Oban.Worker, queue: :images, max_attempts: 3 use Oban.Worker, queue: :images, max_attempts: 3
@impl Oban.Worker @impl Oban.Worker
@ -168,7 +168,7 @@ def upload_image(attrs) do
{:ok, image} -> {:ok, image} ->
# Enqueue async variant generation # Enqueue async variant generation
%{image_id: image.id} %{image_id: image.id}
|> SimpleshopTheme.Workers.ImageVariants.new() |> Berrypod.Workers.ImageVariants.new()
|> Oban.insert() |> Oban.insert()
{:ok, image} {:ok, image}
@ -195,7 +195,7 @@ defp ensure_all_variants do
|> Enum.each(fn image -> |> Enum.each(fn image ->
# Re-enqueue for processing # Re-enqueue for processing
%{image_id: image.id} %{image_id: image.id}
|> SimpleshopTheme.Workers.ImageVariants.new() |> Berrypod.Workers.ImageVariants.new()
|> Oban.insert() |> Oban.insert()
end) end)
end end
@ -223,7 +223,7 @@ end
**File:** `priv/repo/migrations/YYYYMMDDHHMMSS_add_image_metadata.exs` **File:** `priv/repo/migrations/YYYYMMDDHHMMSS_add_image_metadata.exs`
```elixir ```elixir
defmodule SimpleshopTheme.Repo.Migrations.AddImageMetadata do defmodule Berrypod.Repo.Migrations.AddImageMetadata do
use Ecto.Migration use Ecto.Migration
def change do def change do
@ -244,10 +244,10 @@ end
### Step 3: Image Optimizer Module ### Step 3: Image Optimizer Module
**File:** `lib/simpleshop_theme/images/optimizer.ex` **File:** `lib/berrypod/images/optimizer.ex`
```elixir ```elixir
defmodule SimpleshopTheme.Images.Optimizer do defmodule Berrypod.Images.Optimizer do
@moduledoc """ @moduledoc """
Generates optimized image variants. Only creates sizes ≤ source dimensions. Generates optimized image variants. Only creates sizes ≤ source dimensions.
""" """
@ -290,7 +290,7 @@ defmodule SimpleshopTheme.Images.Optimizer do
Called by Oban worker. Called by Oban worker.
""" """
def process_for_image(image_id) do def process_for_image(image_id) do
alias SimpleshopTheme.{Repo, Media.Image} alias Berrypod.{Repo, Media.Image}
case Repo.get(Image, image_id) do case Repo.get(Image, image_id) do
nil -> nil ->
@ -387,16 +387,16 @@ end
### Step 4: Oban Worker ### Step 4: Oban Worker
**File:** `lib/simpleshop_theme/workers/image_variants.ex` **File:** `lib/berrypod/workers/image_variants.ex`
```elixir ```elixir
defmodule SimpleshopTheme.Workers.ImageVariants do defmodule Berrypod.Workers.ImageVariants do
use Oban.Worker, use Oban.Worker,
queue: :images, queue: :images,
max_attempts: 3, max_attempts: 3,
unique: [period: 60] # Prevent duplicate jobs unique: [period: 60] # Prevent duplicate jobs
alias SimpleshopTheme.Images.Optimizer alias Berrypod.Images.Optimizer
@impl Oban.Worker @impl Oban.Worker
def perform(%Oban.Job{args: %{"image_id" => image_id}}) do def perform(%Oban.Job{args: %{"image_id" => image_id}}) do
@ -410,11 +410,11 @@ end
### Step 5: Update Media Module ### Step 5: Update Media Module
**File:** `lib/simpleshop_theme/media.ex` **File:** `lib/berrypod/media.ex`
```elixir ```elixir
alias SimpleshopTheme.Images.Optimizer alias Berrypod.Images.Optimizer
alias SimpleshopTheme.Workers.ImageVariants alias Berrypod.Workers.ImageVariants
def upload_image(attrs) do def upload_image(attrs) do
# Convert to lossless WebP before storing # Convert to lossless WebP before storing
@ -449,10 +449,10 @@ end
### Step 6: Startup Recovery GenServer ### Step 6: Startup Recovery GenServer
**File:** `lib/simpleshop_theme/images/variant_cache.ex` **File:** `lib/berrypod/images/variant_cache.ex`
```elixir ```elixir
defmodule SimpleshopTheme.Images.VariantCache do defmodule Berrypod.Images.VariantCache do
@moduledoc """ @moduledoc """
Ensures all image variants exist on startup. Ensures all image variants exist on startup.
Enqueues Oban jobs for any missing variants. Enqueues Oban jobs for any missing variants.
@ -461,10 +461,10 @@ defmodule SimpleshopTheme.Images.VariantCache do
use GenServer use GenServer
require Logger require Logger
alias SimpleshopTheme.Repo alias Berrypod.Repo
alias SimpleshopTheme.Media.Image, as: ImageSchema alias Berrypod.Media.Image, as: ImageSchema
alias SimpleshopTheme.Images.Optimizer alias Berrypod.Images.Optimizer
alias SimpleshopTheme.Workers.ImageVariants alias Berrypod.Workers.ImageVariants
import Ecto.Query import Ecto.Query
def start_link(_opts) do def start_link(_opts) do
@ -521,7 +521,7 @@ end
### Step 7: Responsive Image Component ### Step 7: Responsive Image Component
**File:** `lib/simpleshop_theme_web/components/shop_components.ex` **File:** `lib/berrypod_web/components/shop_components.ex`
```elixir ```elixir
@doc """ @doc """
@ -547,7 +547,7 @@ attr :height, :integer, default: nil
attr :priority, :boolean, default: false attr :priority, :boolean, default: false
def responsive_image(assigns) do def responsive_image(assigns) do
alias SimpleshopTheme.Images.Optimizer alias Berrypod.Images.Optimizer
# Compute available widths from source dimensions # Compute available widths from source dimensions
available = Optimizer.applicable_widths(assigns.source_width) available = Optimizer.applicable_widths(assigns.source_width)
@ -596,7 +596,7 @@ end
### Step 8: Update Thumbnail Serving ### Step 8: Update Thumbnail Serving
**File:** `lib/simpleshop_theme_web/controllers/image_controller.ex` **File:** `lib/berrypod_web/controllers/image_controller.ex`
```elixir ```elixir
def thumbnail(conn, %{"id" => id}) do def thumbnail(conn, %{"id" => id}) do
@ -750,20 +750,20 @@ end
## File Changes Summary ## File Changes Summary
### Create: ### Create:
- `lib/simpleshop_theme/images/optimizer.ex` - Core optimization logic - `lib/berrypod/images/optimizer.ex` - Core optimization logic
- `lib/simpleshop_theme/images/variant_cache.ex` - Startup recovery GenServer - `lib/berrypod/images/variant_cache.ex` - Startup recovery GenServer
- `lib/simpleshop_theme/workers/image_variants.ex` - Oban worker - `lib/berrypod/workers/image_variants.ex` - Oban worker
- `lib/mix/tasks/optimize_images.ex` - Mockup batch processing - `lib/mix/tasks/optimize_images.ex` - Mockup batch processing
- `priv/repo/migrations/*_add_image_metadata.exs` - Schema + Oban tables - `priv/repo/migrations/*_add_image_metadata.exs` - Schema + Oban tables
### Modify: ### Modify:
- `mix.exs` - Add Oban dependency - `mix.exs` - Add Oban dependency
- `config/config.exs` - Oban configuration - `config/config.exs` - Oban configuration
- `lib/simpleshop_theme/media/image.ex` - Add source_width/height, remove thumbnail_data - `lib/berrypod/media/image.ex` - Add source_width/height, remove thumbnail_data
- `lib/simpleshop_theme/media.ex` - Convert to WebP, enqueue Oban job - `lib/berrypod/media.ex` - Convert to WebP, enqueue Oban job
- `lib/simpleshop_theme/application.ex` - Add Oban + VariantCache to supervision - `lib/berrypod/application.ex` - Add Oban + VariantCache to supervision
- `lib/simpleshop_theme_web/components/shop_components.ex` - Responsive image component - `lib/berrypod_web/components/shop_components.ex` - Responsive image component
- `lib/simpleshop_theme_web/controllers/image_controller.ex` - Serve thumbnails from disk - `lib/berrypod_web/controllers/image_controller.ex` - Serve thumbnails from disk
- `.gitignore` - Add `/priv/static/image_cache/` - `.gitignore` - Add `/priv/static/image_cache/`
--- ---
@ -832,14 +832,14 @@ mix phx.server
**Files to create/modify:** **Files to create/modify:**
- `priv/repo/migrations/*_add_image_metadata.exs` - New migration - `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:** **Manual verification:**
```bash ```bash
# Check migration applied # 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 show new columns: source_width, source_height, variants_status
# Should NOT have: thumbnail_data # Should NOT have: thumbnail_data
``` ```
@ -851,17 +851,17 @@ sqlite3 simpleshop_theme_dev.db ".schema images"
### Phase 3: Optimizer Module ### Phase 3: Optimizer Module
**Files to create:** **Files to create:**
- `lib/simpleshop_theme/images/optimizer.ex` - `lib/berrypod/images/optimizer.ex`
- `test/simpleshop_theme/images/optimizer_test.exs` - `test/berrypod/images/optimizer_test.exs`
- `test/support/fixtures/image_fixtures.ex` - `test/support/fixtures/image_fixtures.ex`
- `test/fixtures/sample_1200x800.jpg` (test image) - `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:** **Manual verification:**
```elixir ```elixir
# In iex -S mix # In iex -S mix
alias SimpleshopTheme.Images.Optimizer alias Berrypod.Images.Optimizer
Optimizer.applicable_widths(1500) # Should return [400, 800, 1200] Optimizer.applicable_widths(1500) # Should return [400, 800, 1200]
Optimizer.applicable_widths(300) # Should return [300] Optimizer.applicable_widths(300) # Should return [300]
``` ```
@ -873,13 +873,13 @@ Optimizer.applicable_widths(300) # Should return [300]
### Phase 4: Oban Worker ### Phase 4: Oban Worker
**Files to create:** **Files to create:**
- `lib/simpleshop_theme/workers/image_variants.ex` - `lib/berrypod/workers/image_variants.ex`
- `test/simpleshop_theme/workers/image_variants_test.exs` - `test/berrypod/workers/image_variants_test.exs`
**Files to modify:** **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:** **Manual verification:**
```bash ```bash
@ -894,15 +894,15 @@ mix phx.server
### Phase 5: Media Module Integration ### Phase 5: Media Module Integration
**Files to modify:** **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:** **Manual verification:**
1. Go to `/admin/theme` 1. Go to `/admin/theme`
2. Upload a new logo image 2. Upload a new logo image
3. Check `priv/static/image_cache/` for generated variants 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` **Commit:** `feat: integrate optimizer with image uploads`
@ -911,10 +911,10 @@ mix phx.server
### Phase 6: VariantCache GenServer ### Phase 6: VariantCache GenServer
**Files to create:** **Files to create:**
- `lib/simpleshop_theme/images/variant_cache.ex` - `lib/berrypod/images/variant_cache.ex`
**Files to modify:** **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) **Tests:** Manual (startup behavior)
@ -934,12 +934,12 @@ mix phx.server
### Phase 7: Responsive Image Component ### Phase 7: Responsive Image Component
**Files to create:** **Files to create:**
- `test/simpleshop_theme_web/components/shop_components_test.exs` - `test/berrypod_web/components/shop_components_test.exs`
**Files to modify:** **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:** **Manual verification:**
```elixir ```elixir
@ -954,7 +954,7 @@ mix phx.server
### Phase 8: ImageController Disk Serving ### Phase 8: ImageController Disk Serving
**Files to modify:** **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 **Tests:** Existing controller tests should pass
@ -1040,8 +1040,8 @@ ls -la priv/static/mockups/
6. **Run tests:** 6. **Run tests:**
```bash ```bash
mix test test/simpleshop_theme/images/ mix test test/berrypod/images/
mix test test/simpleshop_theme/workers/ mix test test/berrypod/workers/
``` ```
7. **Re-run Lighthouse:** 7. **Re-run Lighthouse:**
@ -1063,12 +1063,12 @@ ls -la priv/static/mockups/
``` ```
test/ test/
├── simpleshop_theme/ ├── berrypod/
│ ├── images/ │ ├── images/
│ │ └── optimizer_test.exs # Core optimization logic │ │ └── optimizer_test.exs # Core optimization logic
│ └── workers/ │ └── workers/
│ └── image_variants_test.exs # Oban worker │ └── image_variants_test.exs # Oban worker
├── simpleshop_theme_web/ ├── berrypod_web/
│ └── components/ │ └── components/
│ └── shop_components_test.exs # responsive_image component │ └── shop_components_test.exs # responsive_image component
├── mix/ ├── mix/
@ -1085,16 +1085,16 @@ test/
**`test/support/fixtures/image_fixtures.ex`:** **`test/support/fixtures/image_fixtures.ex`:**
```elixir ```elixir
defmodule SimpleshopTheme.ImageFixtures do defmodule Berrypod.ImageFixtures do
alias SimpleshopTheme.Repo alias Berrypod.Repo
alias SimpleshopTheme.Media.Image alias Berrypod.Media.Image
@sample_jpeg File.read!("test/fixtures/sample_1200x800.jpg") @sample_jpeg File.read!("test/fixtures/sample_1200x800.jpg")
def sample_jpeg, do: @sample_jpeg def sample_jpeg, do: @sample_jpeg
def image_fixture(attrs \\ %{}) do 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 = %{ defaults = %{
image_type: "product", image_type: "product",
@ -1132,23 +1132,23 @@ defmodule SimpleshopTheme.ImageFixtures do
def cache_path(id, width, format) do def cache_path(id, width, format) do
ext = if format == :jpg, do: "jpg", else: Atom.to_string(format) 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 end
def cleanup_cache do 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) if File.exists?(cache_dir), do: File.rm_rf!(cache_dir)
end end
end end
``` ```
**`test/simpleshop_theme/images/optimizer_test.exs`:** **`test/berrypod/images/optimizer_test.exs`:**
```elixir ```elixir
defmodule SimpleshopTheme.Images.OptimizerTest do defmodule Berrypod.Images.OptimizerTest do
use SimpleshopTheme.DataCase, async: false use Berrypod.DataCase, async: false
alias SimpleshopTheme.Images.Optimizer alias Berrypod.Images.Optimizer
import SimpleshopTheme.ImageFixtures import Berrypod.ImageFixtures
setup do setup do
cleanup_cache() cleanup_cache()
@ -1256,15 +1256,15 @@ defmodule SimpleshopTheme.Images.OptimizerTest do
end end
``` ```
**`test/simpleshop_theme/workers/image_variants_test.exs`:** **`test/berrypod/workers/image_variants_test.exs`:**
```elixir ```elixir
defmodule SimpleshopTheme.Workers.ImageVariantsTest do defmodule Berrypod.Workers.ImageVariantsTest do
use SimpleshopTheme.DataCase, async: false use Berrypod.DataCase, async: false
use Oban.Testing, repo: SimpleshopTheme.Repo use Oban.Testing, repo: Berrypod.Repo
alias SimpleshopTheme.Workers.ImageVariants alias Berrypod.Workers.ImageVariants
alias SimpleshopTheme.Media.Image alias Berrypod.Media.Image
import SimpleshopTheme.ImageFixtures import Berrypod.ImageFixtures
setup do setup do
cleanup_cache() cleanup_cache()
@ -1297,13 +1297,13 @@ defmodule SimpleshopTheme.Workers.ImageVariantsTest do
end end
``` ```
**`test/simpleshop_theme_web/components/shop_components_test.exs`:** **`test/berrypod_web/components/shop_components_test.exs`:**
```elixir ```elixir
defmodule SimpleshopThemeWeb.ShopComponentsTest do defmodule BerrypodWeb.ShopComponentsTest do
use SimpleshopThemeWeb.ConnCase, async: true use BerrypodWeb.ConnCase, async: true
import Phoenix.LiveViewTest import Phoenix.LiveViewTest
alias SimpleshopThemeWeb.ShopComponents alias BerrypodWeb.ShopComponents
describe "responsive_image/1" do describe "responsive_image/1" do
test "builds srcset with all widths for 1200px source" do test "builds srcset with all widths for 1200px source" do

View File

@ -51,7 +51,7 @@ PageRenderer module
### PageLayout Schema ### PageLayout Schema
```elixir ```elixir
defmodule SimpleshopTheme.Content.PageLayout do defmodule Berrypod.Content.PageLayout do
use Ecto.Schema use Ecto.Schema
schema "page_layouts" do schema "page_layouts" do
@ -59,7 +59,7 @@ defmodule SimpleshopTheme.Content.PageLayout do
field :name, :string # Display name for the layout field :name, :string # Display name for the layout
field :is_default, :boolean, default: false field :is_default, :boolean, default: false
has_many :sections, SimpleshopTheme.Content.PageSection has_many :sections, Berrypod.Content.PageSection
timestamps() timestamps()
end end
@ -69,7 +69,7 @@ end
### PageSection Schema ### PageSection Schema
```elixir ```elixir
defmodule SimpleshopTheme.Content.PageSection do defmodule Berrypod.Content.PageSection do
use Ecto.Schema use Ecto.Schema
schema "page_sections" do schema "page_sections" do
@ -78,7 +78,7 @@ defmodule SimpleshopTheme.Content.PageSection do
field :settings, :map # JSON settings for the section field :settings, :map # JSON settings for the section
field :enabled, :boolean, default: true field :enabled, :boolean, default: true
belongs_to :page_layout, SimpleshopTheme.Content.PageLayout belongs_to :page_layout, Berrypod.Content.PageLayout
timestamps() timestamps()
end end
@ -88,11 +88,11 @@ end
### Section Types Registry ### Section Types Registry
```elixir ```elixir
defmodule SimpleshopTheme.Content.SectionTypes do defmodule Berrypod.Content.SectionTypes do
@sections %{ @sections %{
"hero" => %{ "hero" => %{
name: "Hero Banner", name: "Hero Banner",
component: &SimpleshopThemeWeb.ShopComponents.hero_section/1, component: &BerrypodWeb.ShopComponents.hero_section/1,
settings_schema: %{ settings_schema: %{
title: %{type: :string, default: "Welcome"}, title: %{type: :string, default: "Welcome"},
description: %{type: :string, default: ""}, description: %{type: :string, default: ""},
@ -104,7 +104,7 @@ defmodule SimpleshopTheme.Content.SectionTypes do
}, },
"featured_products" => %{ "featured_products" => %{
name: "Featured Products", name: "Featured Products",
component: &SimpleshopThemeWeb.ShopComponents.featured_products_section/1, component: &BerrypodWeb.ShopComponents.featured_products_section/1,
settings_schema: %{ settings_schema: %{
title: %{type: :string, default: "Featured products"}, title: %{type: :string, default: "Featured products"},
product_count: %{type: :integer, default: 8} product_count: %{type: :integer, default: 8}
@ -113,13 +113,13 @@ defmodule SimpleshopTheme.Content.SectionTypes do
}, },
"category_nav" => %{ "category_nav" => %{
name: "Category Navigation", name: "Category Navigation",
component: &SimpleshopThemeWeb.ShopComponents.category_nav/1, component: &BerrypodWeb.ShopComponents.category_nav/1,
settings_schema: %{}, settings_schema: %{},
allowed_on: [:home] allowed_on: [:home]
}, },
"image_text" => %{ "image_text" => %{
name: "Image + Text Block", name: "Image + Text Block",
component: &SimpleshopThemeWeb.ShopComponents.image_text_section/1, component: &BerrypodWeb.ShopComponents.image_text_section/1,
settings_schema: %{ settings_schema: %{
title: %{type: :string}, title: %{type: :string},
description: %{type: :text}, description: %{type: :text},
@ -131,7 +131,7 @@ defmodule SimpleshopTheme.Content.SectionTypes do
}, },
"content_body" => %{ "content_body" => %{
name: "Rich Text Content", name: "Rich Text Content",
component: &SimpleshopThemeWeb.ShopComponents.content_body/1, component: &BerrypodWeb.ShopComponents.content_body/1,
settings_schema: %{ settings_schema: %{
image_url: %{type: :image}, image_url: %{type: :image},
content: %{type: :rich_text} content: %{type: :rich_text}
@ -140,13 +140,13 @@ defmodule SimpleshopTheme.Content.SectionTypes do
}, },
"reviews_section" => %{ "reviews_section" => %{
name: "Customer Reviews", name: "Customer Reviews",
component: &SimpleshopThemeWeb.ShopComponents.reviews_section/1, component: &BerrypodWeb.ShopComponents.reviews_section/1,
settings_schema: %{}, settings_schema: %{},
allowed_on: [:pdp] allowed_on: [:pdp]
}, },
"related_products" => %{ "related_products" => %{
name: "Related Products", name: "Related Products",
component: &SimpleshopThemeWeb.ShopComponents.related_products_section/1, component: &BerrypodWeb.ShopComponents.related_products_section/1,
settings_schema: %{}, settings_schema: %{},
allowed_on: [:pdp] allowed_on: [:pdp]
} }
@ -157,11 +157,11 @@ end
## Page Renderer ## Page Renderer
```elixir ```elixir
defmodule SimpleshopThemeWeb.PageRenderer do defmodule BerrypodWeb.PageRenderer do
use Phoenix.Component use Phoenix.Component
import SimpleshopThemeWeb.ShopComponents import BerrypodWeb.ShopComponents
alias SimpleshopTheme.Content.SectionTypes alias Berrypod.Content.SectionTypes
@doc """ @doc """
Renders a page from its layout and data context. Renders a page from its layout and data context.
@ -394,6 +394,6 @@ Page layouts should be cached since they change infrequently:
## Related Files ## Related Files
- `lib/simpleshop_theme_web/components/shop_components.ex` - Existing section components - `lib/berrypod_web/components/shop_components.ex` - Existing section components
- `lib/simpleshop_theme_web/components/page_templates/` - Current static templates (will become defaults) - `lib/berrypod_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/live/theme_live/index.ex` - Theme editor (reference implementation)

View File

@ -36,7 +36,7 @@ This simplification means `provider_data` stores different fields and shipping r
### 1.1 HTTP client ### 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. 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 ### 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. 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 ### 1.3 Wire into Provider dispatch
**File:** `lib/simpleshop_theme/providers/provider.ex` **File:** `lib/berrypod/providers/provider.ex`
Change line 97: Change line 97:
```elixir ```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 ### 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. 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 ### 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: 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 ### 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: 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) ### New files (6)
| File | Purpose | | File | Purpose |
|------|---------| |------|---------|
| `lib/simpleshop_theme/clients/printful.ex` | HTTP client | | `lib/berrypod/clients/printful.ex` | HTTP client |
| `lib/simpleshop_theme/providers/printful.ex` | Provider behaviour implementation | | `lib/berrypod/providers/printful.ex` | Provider behaviour implementation |
| `lib/simpleshop_theme/sync/mockup_generation_worker.ex` | Async mockup generation | | `lib/berrypod/sync/mockup_generation_worker.ex` | Async mockup generation |
| `lib/simpleshop_theme_web/controllers/printful_webhook_controller.ex` | Webhook handler | | `lib/berrypod_web/controllers/printful_webhook_controller.ex` | Webhook handler |
| `test/simpleshop_theme/providers/printful_test.exs` | Provider tests | | `test/berrypod/providers/printful_test.exs` | Provider tests |
| `test/simpleshop_theme/clients/printful_test.exs` | Client tests (with Req mocking) | | `test/berrypod/clients/printful_test.exs` | Client tests (with Req mocking) |
### Modified files (6) ### Modified files (6)
| File | Change | | File | Change |
|------|--------| |------|--------|
| `lib/simpleshop_theme/providers/provider.ex` | Wire `"printful"` to Printful module | | `lib/berrypod/providers/provider.ex` | Wire `"printful"` to Printful module |
| `lib/simpleshop_theme/orders.ex` | Route to correct provider per order (not hardcoded "printify") | | `lib/berrypod/orders.ex` | Route to correct provider per order (not hardcoded "printify") |
| `lib/simpleshop_theme_web/router.ex` | Add Printful webhook route | | `lib/berrypod_web/router.ex` | Add Printful webhook route |
| `config/config.exs` | (optional) Printful-specific config | | `config/config.exs` | (optional) Printful-specific config |
| `lib/simpleshop_theme/sync/product_sync_worker.ex` | Enqueue mockup generation for Printful products | | `lib/berrypod/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_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 ## 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. **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.

View File

@ -74,17 +74,17 @@ The official Shopify integration handles product identity well through SKUs.
- Apply at printify.com/printify-api - Apply at printify.com/printify-api
- ~1 week approval process - ~1 week approval process
- Merchants authorize SimpleShop via OAuth - Merchants authorize Berrypod via OAuth
- We appear in Printify's publishing UI alongside Shopify/Etsy - We appear in Printify's publishing UI alongside Shopify/Etsy
- Products get "published" TO us with lock - Products get "published" TO us with lock
- Webhooks for real-time updates - Webhooks for real-time updates
**How publishing works in Option 2:** **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 2. Printify sends `product:publish:started` webhook
3. We create the product on our side 3. We create the product on our side
4. We call `publishing_succeeded.json` to confirm 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:** **Benefits of official integration:**
- Publish lock prevents editing after publishing (data consistency) - Publish lock prevents editing after publishing (data consistency)
@ -189,7 +189,7 @@ From Printify documentation:
### The core tension ### The core tension
SimpleShop exists in two forms: Berrypod exists in two forms:
1. **Open source** - self-hosted by anyone 1. **Open source** - self-hosted by anyone
2. **Managed hosting** - SaaS service run by us 2. **Managed hosting** - SaaS service run by us
@ -199,7 +199,7 @@ Printify's integration options have different implications for each.
**How it works:** **How it works:**
- Each merchant creates their own Printify API token - Each merchant creates their own Printify API token
- Token entered in SimpleShop admin - Token entered in Berrypod admin
- Direct API access, no intermediary - Direct API access, no intermediary
**Open source:** ✅ Works perfectly **Open source:** ✅ Works perfectly
@ -222,7 +222,7 @@ Printify's integration options have different implications for each.
### OAuth Platform Integration ### OAuth Platform Integration
**How it works:** **How it works:**
- Register SimpleShop as an app with Printify - Register Berrypod as an app with Printify
- Get OAuth client ID/secret - Get OAuth client ID/secret
- Merchants click "Connect" and authorize - Merchants click "Connect" and authorize
- We receive access tokens, appear in Printify UI - 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 | | Webhook proxy | Depends on proxy | Full features |
**Potential differentiation for managed hosting:** **Potential differentiation for managed hosting:**
- Official "Publish to SimpleShop" in Printify UI - Official "Publish to Berrypod" in Printify UI
- Real-time sync via webhooks - Real-time sync via webhooks
- Publish lock (data consistency guarantee) - Publish lock (data consistency guarantee)
- Pre-checkout validation (verify before order) - Pre-checkout validation (verify before order)

View File

@ -14,7 +14,7 @@ Build a Products context that syncs products from external POD providers (Printi
## Current Domain Analysis ## 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 | | 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 │ │ │ │ WEB LAYER │ │
│ │ SimpleshopThemeWeb │ │ │ │ BerrypodWeb │ │
│ │ ┌────────────────┐ ┌────────────────┐ ┌─────────────────────────┐ │ │ │ │ ┌────────────────┐ ┌────────────────┐ ┌─────────────────────────┐ │ │
│ │ │ Shop LiveViews │ │ Admin LiveViews│ │ Theme Editor LiveView │ │ │ │ │ │ Shop LiveViews │ │ Admin LiveViews│ │ Theme Editor LiveView │ │ │
│ │ │ - ProductShow │ │ - UserLogin │ │ - ThemeLive.Index │ │ │ │ │ │ - ProductShow │ │ - UserLogin │ │ - ThemeLive.Index │ │ │
@ -299,22 +299,22 @@ Extract reusable helpers into new `Printify.Catalog` module:
**Module structure:** **Module structure:**
``` ```
lib/simpleshop_theme/clients/ lib/berrypod/clients/
├── printify.ex # Printify HTTP client (moved from printify/client.ex) ├── printify.ex # Printify HTTP client (moved from printify/client.ex)
├── gelato.ex # Gelato HTTP client ├── gelato.ex # Gelato HTTP client
└── prodigi.ex # Prodigi HTTP client └── prodigi.ex # Prodigi HTTP client
lib/simpleshop_theme/providers/ lib/berrypod/providers/
├── provider.ex # Behaviour definition ├── provider.ex # Behaviour definition
├── printify.ex # Printify implementation (uses Clients.Printify) ├── printify.ex # Printify implementation (uses Clients.Printify)
├── gelato.ex # Gelato implementation (uses Clients.Gelato) ├── gelato.ex # Gelato implementation (uses Clients.Gelato)
└── prodigi.ex # Prodigi implementation (uses Clients.Prodigi) └── prodigi.ex # Prodigi implementation (uses Clients.Prodigi)
lib/simpleshop_theme/mockups/ lib/berrypod/mockups/
└── generator.ex # Mockup generation (currently uses Clients.Printify) └── generator.ex # Mockup generation (currently uses Clients.Printify)
# Provider-agnostic location for future flexibility # Provider-agnostic location for future flexibility
lib/simpleshop_theme/printify/ lib/berrypod/printify/
└── catalog.ex # Blueprint/variant discovery helpers (Printify-specific) └── 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]` - Unique constraint: `[:provider_connection_id, :provider_product_id]`
2. **Credentials encrypted in database** 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 - `api_key_encrypted`, `oauth_access_token_encrypted` fields
3. **Cost tracking for profit calculation** 3. **Cost tracking for profit calculation**
@ -832,46 +832,46 @@ Add to `mix.exs`:
- `*_create_admin_notifications.exs` - `*_create_admin_notifications.exs`
### Schemas ### Schemas
- `lib/simpleshop_theme/products/provider_connection.ex` - `lib/berrypod/products/provider_connection.ex`
- `lib/simpleshop_theme/products/product.ex` - `lib/berrypod/products/product.ex`
- `lib/simpleshop_theme/products/product_image.ex` - `lib/berrypod/products/product_image.ex`
- `lib/simpleshop_theme/products/product_variant.ex` - `lib/berrypod/products/product_variant.ex`
- `lib/simpleshop_theme/orders/order.ex` - `lib/berrypod/orders/order.ex`
- `lib/simpleshop_theme/orders/order_fulfillment.ex` - `lib/berrypod/orders/order_fulfillment.ex`
- `lib/simpleshop_theme/orders/order_line_item.ex` - `lib/berrypod/orders/order_line_item.ex`
- `lib/simpleshop_theme/orders/order_event.ex` - `lib/berrypod/orders/order_event.ex`
- `lib/simpleshop_theme/admin_notifications/notification.ex` - `lib/berrypod/admin_notifications/notification.ex`
### Contexts ### Contexts
- `lib/simpleshop_theme/products.ex` - Product queries, sync logic - `lib/berrypod/products.ex` - Product queries, sync logic
- `lib/simpleshop_theme/orders.ex` - Order creation, submission - `lib/berrypod/orders.ex` - Order creation, submission
- `lib/simpleshop_theme/admin_notifications.ex` - Admin notification management - `lib/berrypod/admin_notifications.ex` - Admin notification management
### Providers ### Providers
- `lib/simpleshop_theme/providers/provider.ex` - Behaviour definition - `lib/berrypod/providers/provider.ex` - Behaviour definition
- `lib/simpleshop_theme/providers/printify.ex` - Printify implementation - `lib/berrypod/providers/printify.ex` - Printify implementation
### Workers ### Workers
- `lib/simpleshop_theme/sync/product_sync_worker.ex` - Oban worker - `lib/berrypod/sync/product_sync_worker.ex` - Oban worker
### Webhooks ### Webhooks
- `lib/simpleshop_theme_web/controllers/webhook_controller.ex` - `lib/berrypod_web/controllers/webhook_controller.ex`
- `lib/simpleshop_theme/webhooks/printify_handler.ex` - `lib/berrypod/webhooks/printify_handler.ex`
### Notifiers ### Notifiers
- `lib/simpleshop_theme_web/notifiers/customer_notifier.ex` - Customer emails - `lib/berrypod_web/notifiers/customer_notifier.ex` - Customer emails
### Support ### Support
- `lib/simpleshop_theme/vault.ex` - Credential encryption - `lib/berrypod/vault.ex` - Credential encryption
--- ---
## Files to Modify ## Files to Modify
- `lib/simpleshop_theme/printify/client.ex` → Move to `lib/simpleshop_theme/clients/printify.ex` - `lib/berrypod/printify/client.ex` → Move to `lib/berrypod/clients/printify.ex`
- `lib/simpleshop_theme/printify/mockup_generator.ex` → Move to `lib/simpleshop_theme/mockups/generator.ex` - `lib/berrypod/printify/mockup_generator.ex` → Move to `lib/berrypod/mockups/generator.ex`
- `lib/simpleshop_theme/theme/preview_data.ex` - Query real products when available - `lib/berrypod/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_web/live/shop_live/*.ex` - Use Products context instead of PreviewData
--- ---
@ -943,10 +943,10 @@ end
``` ```
### Files to Create ### Files to Create
- `lib/simpleshop_theme/admin_notifications.ex` - Admin notification context - `lib/berrypod/admin_notifications.ex` - Admin notification context
- `lib/simpleshop_theme/admin_notifications/notification.ex` - Schema - `lib/berrypod/admin_notifications/notification.ex` - Schema
- `lib/simpleshop_theme/orders/order_event.ex` - Customer-facing event schema - `lib/berrypod/orders/order_event.ex` - Customer-facing event schema
- `lib/simpleshop_theme_web/notifiers/customer_notifier.ex` - Emails - `lib/berrypod_web/notifiers/customer_notifier.ex` - Emails
--- ---
@ -1012,15 +1012,15 @@ end
**Mocking External APIs (Mox pattern):** **Mocking External APIs (Mox pattern):**
```elixir ```elixir
# test/support/mocks.ex # 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/test.exs
config :simpleshop_theme, :printify_client, SimpleshopTheme.Clients.MockPrintify config :berrypod, :printify_client, Berrypod.Clients.MockPrintify
``` ```
**Oban testing:** **Oban testing:**
```elixir ```elixir
use Oban.Testing, repo: SimpleshopTheme.Repo use Oban.Testing, repo: Berrypod.Repo
# Jobs run synchronously in tests via perform_job/2 # Jobs run synchronously in tests via perform_job/2
``` ```
@ -1049,7 +1049,7 @@ use Oban.Testing, repo: SimpleshopTheme.Repo
### Example Test Cases ### Example Test Cases
```elixir ```elixir
# test/simpleshop_theme/products_test.exs # test/berrypod/products_test.exs
describe "sync_products/1" do describe "sync_products/1" do
test "syncs products from provider" do test "syncs products from provider" do
conn = provider_connection_fixture() conn = provider_connection_fixture()
@ -1082,7 +1082,7 @@ describe "sync_products/1" do
end end
end end
# test/simpleshop_theme/orders_test.exs # test/berrypod/orders_test.exs
describe "create_order_from_cart/1" do describe "create_order_from_cart/1" do
test "splits cart into fulfillments by provider" do test "splits cart into fulfillments by provider" do
printify_variant = variant_fixture(provider: :printify) printify_variant = variant_fixture(provider: :printify)
@ -1098,7 +1098,7 @@ describe "create_order_from_cart/1" do
end end
end end
# test/simpleshop_theme/sync/product_sync_worker_test.exs # test/berrypod/sync/product_sync_worker_test.exs
describe "perform/1" do describe "perform/1" do
test "retries on API failure" do test "retries on API failure" do
expect(MockPrintify, :list_products, fn _ -> {:error, :timeout} end) expect(MockPrintify, :list_products, fn _ -> {:error, :timeout} end)
@ -1197,12 +1197,12 @@ live "/admin/providers/:id/edit", ProviderLive.Index, :edit
| File | Purpose | | File | Purpose |
|------|---------| |------|---------|
| `lib/simpleshop_theme_web/live/provider_live/index.ex` | LiveView for provider list + modal forms | | `lib/berrypod_web/live/provider_live/index.ex` | LiveView for provider list + modal forms |
| `lib/simpleshop_theme_web/live/provider_live/index.html.heex` | Template | | `lib/berrypod_web/live/provider_live/index.html.heex` | Template |
| `lib/simpleshop_theme_web/live/provider_live/form_component.ex` | Form component for new/edit | | `lib/berrypod_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/berrypod/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) | | `lib/berrypod/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 | | `test/berrypod_web/live/provider_live_test.exs` | LiveView tests |
### UI Design ### UI Design
@ -1264,11 +1264,11 @@ Single-page admin with modal for add/edit (follows Phoenix generator pattern):
**Index LiveView (`provider_live/index.ex`):** **Index LiveView (`provider_live/index.ex`):**
```elixir ```elixir
defmodule SimpleshopThemeWeb.ProviderLive.Index do defmodule BerrypodWeb.ProviderLive.Index do
use SimpleshopThemeWeb, :live_view use BerrypodWeb, :live_view
alias SimpleshopTheme.Products alias Berrypod.Products
alias SimpleshopTheme.Products.ProviderConnection alias Berrypod.Products.ProviderConnection
@impl true @impl true
def mount(_params, _session, socket) do def mount(_params, _session, socket) do
@ -1300,7 +1300,7 @@ defmodule SimpleshopThemeWeb.ProviderLive.Index do
end end
@impl true @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)} {:noreply, stream_insert(socket, :connections, connection)}
end end
@ -1351,7 +1351,7 @@ end
defp test_provider_connection("printify", api_key) do defp test_provider_connection("printify", api_key) do
# Build temporary connection struct for testing # Build temporary connection struct for testing
conn = %ProviderConnection{provider_type: "printify", api_key: api_key} conn = %ProviderConnection{provider_type: "printify", api_key: api_key}
SimpleshopTheme.Providers.Printify.test_connection(conn) Berrypod.Providers.Printify.test_connection(conn)
end end
``` ```
@ -1404,7 +1404,7 @@ def handle_event("save", %{"provider" => params}, socket) do
end end
defp register_webhooks(%{provider_type: "printify"} = conn) do defp register_webhooks(%{provider_type: "printify"} = conn) do
SimpleshopTheme.Providers.Printify.register_webhooks(conn) Berrypod.Providers.Printify.register_webhooks(conn)
end end
``` ```
@ -1422,7 +1422,7 @@ def handle_event("delete", %{"id" => id}, socket) do
end end
defp unregister_webhooks(%{provider_type: "printify"} = conn) do defp unregister_webhooks(%{provider_type: "printify"} = conn) do
SimpleshopTheme.Providers.Printify.unregister_webhooks(conn) Berrypod.Providers.Printify.unregister_webhooks(conn)
end end
``` ```
@ -1432,7 +1432,7 @@ end
@webhook_topics ~w(product:publish:started product:deleted shop:disconnected) @webhook_topics ~w(product:publish:started product:deleted shop:disconnected)
def register_webhooks(conn) do 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) shop_id = get_shop_id(conn)
results = Enum.map(@webhook_topics, fn topic -> results = Enum.map(@webhook_topics, fn topic ->
@ -1455,7 +1455,7 @@ def unregister_webhooks(conn) do
# List existing webhooks and delete ours # List existing webhooks and delete ours
case Client.get(conn, "/shops/#{shop_id}/webhooks.json") do case Client.get(conn, "/shops/#{shop_id}/webhooks.json") do
{:ok, %{"webhooks" => webhooks}} -> {:ok, %{"webhooks" => webhooks}} ->
our_url = SimpleshopThemeWeb.Endpoint.url() <> "/webhooks/printify" our_url = BerrypodWeb.Endpoint.url() <> "/webhooks/printify"
webhooks webhooks
|> Enum.filter(&(&1["url"] == our_url)) |> Enum.filter(&(&1["url"] == our_url))
@ -1504,7 +1504,7 @@ Template snippet:
### Context Additions ### Context Additions
Add to `lib/simpleshop_theme/products.ex`: Add to `lib/berrypod/products.ex`:
```elixir ```elixir
@doc """ @doc """
@ -1513,7 +1513,7 @@ Returns {:ok, job} or {:error, changeset}.
""" """
def enqueue_sync(%ProviderConnection{} = conn) do def enqueue_sync(%ProviderConnection{} = conn) do
%{connection_id: conn.id} %{connection_id: conn.id}
|> SimpleshopTheme.Workers.ProductSyncWorker.new() |> Berrypod.Workers.ProductSyncWorker.new()
|> Oban.insert() |> Oban.insert()
end end
@ -1543,7 +1543,7 @@ end
### Testing ### Testing
```elixir ```elixir
# test/simpleshop_theme_web/live/provider_live_test.exs # test/berrypod_web/live/provider_live_test.exs
describe "Index" do describe "Index" do
setup :register_and_log_in_user setup :register_and_log_in_user
@ -1619,25 +1619,25 @@ Implement a robust product sync strategy with three mechanisms:
| File | Purpose | | File | Purpose |
|------|---------| |------|---------|
| `lib/simpleshop_theme/workers/product_sync_worker.ex` | Oban worker for full/single product sync | | `lib/berrypod/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/berrypod_web/controllers/webhook_controller.ex` | Receives webhooks from providers |
| `lib/simpleshop_theme/webhooks/printify_handler.ex` | Printify-specific webhook processing | | `lib/berrypod/webhooks/printify_handler.ex` | Printify-specific webhook processing |
| `test/simpleshop_theme/workers/product_sync_worker_test.exs` | Worker tests | | `test/berrypod/workers/product_sync_worker_test.exs` | Worker tests |
| `test/simpleshop_theme_web/controllers/webhook_controller_test.exs` | Webhook endpoint tests | | `test/berrypod_web/controllers/webhook_controller_test.exs` | Webhook endpoint tests |
### Part 1: ProductSyncWorker (~1hr) ### Part 1: ProductSyncWorker (~1hr)
Oban worker that syncs products from a provider connection. Oban worker that syncs products from a provider connection.
```elixir ```elixir
defmodule SimpleshopTheme.Workers.ProductSyncWorker do defmodule Berrypod.Workers.ProductSyncWorker do
use Oban.Worker, use Oban.Worker,
queue: :sync, queue: :sync,
max_attempts: 3, max_attempts: 3,
unique: [period: 60, fields: [:args, :queue]] unique: [period: 60, fields: [:args, :queue]]
alias SimpleshopTheme.Products alias Berrypod.Products
alias SimpleshopTheme.Providers alias Berrypod.Providers
@impl Oban.Worker @impl Oban.Worker
def perform(%Oban.Job{args: %{"connection_id" => conn_id} = args}) do def perform(%Oban.Job{args: %{"connection_id" => conn_id} = args}) do
@ -1714,10 +1714,10 @@ post "/webhooks/printify", WebhookController, :printify
**Controller:** **Controller:**
```elixir ```elixir
defmodule SimpleshopThemeWeb.WebhookController do defmodule BerrypodWeb.WebhookController do
use SimpleshopThemeWeb, :controller use BerrypodWeb, :controller
alias SimpleshopTheme.Webhooks.PrintifyHandler alias Berrypod.Webhooks.PrintifyHandler
def printify(conn, params) do def printify(conn, params) do
with :ok <- verify_printify_signature(conn), with :ok <- verify_printify_signature(conn),
@ -1737,7 +1737,7 @@ defmodule SimpleshopThemeWeb.WebhookController do
# Header: X-Printify-Signature # Header: X-Printify-Signature
signature = get_req_header(conn, "x-printify-signature") |> List.first() signature = get_req_header(conn, "x-printify-signature") |> List.first()
body = conn.assigns[:raw_body] 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) expected = :crypto.mac(:hmac, :sha256, secret, body) |> Base.encode16(case: :lower)
@ -1752,9 +1752,9 @@ end
**Handler:** **Handler:**
```elixir ```elixir
defmodule SimpleshopTheme.Webhooks.PrintifyHandler do defmodule Berrypod.Webhooks.PrintifyHandler do
alias SimpleshopTheme.Products alias Berrypod.Products
alias SimpleshopTheme.Workers.ProductSyncWorker alias Berrypod.Workers.ProductSyncWorker
def handle(%{"type" => "product:publish:started", "resource" => resource}) do def handle(%{"type" => "product:publish:started", "resource" => resource}) do
%{"shop_id" => shop_id, "id" => product_id} = resource %{"shop_id" => shop_id, "id" => product_id} = resource
@ -1800,16 +1800,16 @@ end
Add to Oban config for daily fallback: Add to Oban config for daily fallback:
```elixir ```elixir
# In config/config.exs # In config/config.exs
config :simpleshop_theme, Oban, config :berrypod, Oban,
plugins: [ plugins: [
{Oban.Plugins.Cron, crontab: [ {Oban.Plugins.Cron, crontab: [
{"0 3 * * *", SimpleshopTheme.Workers.ScheduledSyncWorker} # 3 AM daily {"0 3 * * *", Berrypod.Workers.ScheduledSyncWorker} # 3 AM daily
]} ]}
] ]
``` ```
```elixir ```elixir
defmodule SimpleshopTheme.Workers.ScheduledSyncWorker do defmodule Berrypod.Workers.ScheduledSyncWorker do
use Oban.Worker, queue: :sync use Oban.Worker, queue: :sync
def perform(_job) do def perform(_job) do
@ -1827,7 +1827,7 @@ end
### Context Additions ### Context Additions
Add to `lib/simpleshop_theme/products.ex`: Add to `lib/berrypod/products.ex`:
```elixir ```elixir
def archive_product_by_provider(connection_id, provider_product_id) do def archive_product_by_provider(connection_id, provider_product_id) do

View File

@ -2,7 +2,7 @@
**Status:** Reference (research notes from Feb 2025) **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) ### 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: Printful wins because:
- **Mockup generation API** — dedicated async endpoint, generates mockups on actual product blanks - **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) - **`Provider.for_type/1`** dispatch (currently Printify-only, extensible via app env)
Adding Prodigi requires: Adding Prodigi requires:
1. `lib/simpleshop_theme/clients/prodigi.ex` — HTTP client 1. `lib/berrypod/clients/prodigi.ex` — HTTP client
2. `lib/simpleshop_theme/providers/prodigi.ex` — Provider behaviour implementation 2. `lib/berrypod/providers/prodigi.ex` — Provider behaviour implementation
3. Migration: update provider_connections to support "prodigi" type 3. Migration: update provider_connections to support "prodigi" type
4. Admin UI: provider selection during setup 4. Admin UI: provider selection during setup

View File

@ -15,7 +15,7 @@ Product maps have: `.name`, `.category`, `.description`, `.slug`, `.price`, `.im
## Changes ## Changes
### 1. CartHook — add search assigns + event handler ### 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: ""` - Init assigns in `on_mount`: `search_results: []`, `search_query: ""`
- Handle `"search"` event (from `phx-keyup`): - 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 - Handle `"close_search"` event → clear query + results + hide modal via JS
### 2. shop_layout + search_modal — add search attrs and UI ### 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:** **shop_layout:**
- Add optional attrs: `search_results` (default `[]`), `search_query` (default `""`) - 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) - Click on result: navigate to product, close modal (JS.hide + close_search event)
### 3. Page templates — thread search assigns ### 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: 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`. Same pattern as `cart_drawer_open` and `cart_status`.
## Files to modify ## Files to modify
1. `lib/simpleshop_theme_web/cart_hook.ex` 1. `lib/berrypod_web/cart_hook.ex`
2. `lib/simpleshop_theme_web/components/shop_components/layout.ex` (shop_layout + search_modal) 2. `lib/berrypod_web/components/shop_components/layout.ex` (shop_layout + search_modal)
3. All 8 page templates in `lib/simpleshop_theme_web/components/page_templates/` 3. All 8 page templates in `lib/berrypod_web/components/page_templates/`
## Verification ## Verification
- Browser: open search modal on multiple pages, type queries, verify results appear and link correctly - Browser: open search modal on multiple pages, type queries, verify results appear and link correctly

View File

@ -28,7 +28,7 @@ This plan covers the first piece. The other two are separate tasks that build on
## Design: single-admin, closed registration ## 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 ### Fresh install flow
@ -62,28 +62,28 @@ The setup wizard checks three things before allowing "go live":
| Step | How to check | Module/function | | 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` | | 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` | `SimpleshopTheme.Products` | | Products synced | `Products.count_products_for_connection(conn.id) > 0` | `Berrypod.Products` |
| Stripe connected | `Settings.has_secret?("stripe_api_key")` | `SimpleshopTheme.Settings` | | Stripe connected | `Settings.has_secret?("stripe_api_key")` | `Berrypod.Settings` |
Optional (nice-to-have, not blocking go-live): Optional (nice-to-have, not blocking go-live):
- Stripe webhook configured: `Settings.has_secret?("stripe_webhook_signing_secret")` - 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 ## Changes
### 1. `Accounts.has_admin?/0` and registration lockdown ### 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) - Add `has_admin?/0``Repo.exists?(User)` (any user = admin exists)
- This is the single check that gates registration - 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" - 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 - 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 ### 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 `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)` - Add `set_site_live/1` — writes `put_setting("shop", "site_live", value)`
No migration needed — settings table already stores arbitrary key/value pairs. 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: - `setup_status/0` returns a map:
```elixir ```elixir
@ -118,24 +118,24 @@ No migration needed — settings table already stores arbitrary key/value pairs.
### 3. Fresh install redirect (no admin exists) ### 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: ThemeHook already runs on every public shop page mount. Add early check:
- If `Accounts.has_admin?()` is false → redirect to `/setup` - If `Accounts.has_admin?()` is false → redirect to `/setup`
- This catches the fresh install case before any other logic runs - 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: A simple public LiveView at `/setup` that:
- If admin already exists → redirect to `/users/log-in` - 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 - On submit → calls `Accounts.register_user/1` and sends magic link
- Shows "Check your email" confirmation - Shows "Check your email" confirmation
This reuses the existing registration logic but with a different UI (setup-focused, not generic registration). 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) - 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 ### 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): Extend the ThemeHook logic (after the fresh install check):
- If `site_live?()` is false AND user is not authenticated → redirect to `/coming-soon` - 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: Minimal LiveView:
- Uses the shop root layout (gets theme styling) but no nav/footer - Uses the shop root layout (gets theme styling) but no nav/footer
- Shows site name/logo, "Coming soon" heading, optional tagline - Shows site name/logo, "Coming soon" heading, optional tagline
- No redirect loop — this page itself doesn't trigger the gate - 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) - 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 ### 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: 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. Each step shows what to do and links to where to do it. Feels like guided onboarding, not a settings dump.
**Files:** **Files:**
- `lib/simpleshop_theme_web/live/admin/setup_live.ex` - `lib/berrypod_web/live/admin/setup_live.ex`
- Router update to add the route - Router update to add the route
- Admin nav update to include "Setup" link (prominent when not live) - 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 ### 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 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 - 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 ### 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 `/` - 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) - Subsequent logins go to `/` as normal (or `/admin/setup` if not live yet)

View File

@ -24,7 +24,7 @@ Fetch shipping rates from Printify during product sync, cache them in DB, displa
## 1. Migration + schema: `shipping_rates` ## 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: 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` ## 3. Provider behaviour: add `fetch_shipping_rates/2`
**File:** `lib/simpleshop_theme/providers/provider.ex` **File:** `lib/berrypod/providers/provider.ex`
```elixir ```elixir
@callback fetch_shipping_rates(ProviderConnection.t(), products :: [map()]) :: @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 ## 4. Printify implementation
**File:** `lib/simpleshop_theme/providers/printify.ex` **File:** `lib/berrypod/providers/printify.ex`
New function `fetch_shipping_rates/2`: New function `fetch_shipping_rates/2`:
@ -90,7 +90,7 @@ New function `fetch_shipping_rates/2`:
## 5. Shipping context ## 5. Shipping context
**New file:** `lib/simpleshop_theme/shipping.ex` **New file:** `lib/berrypod/shipping.ex`
Functions: Functions:
- `upsert_rates(provider_connection_id, rates)` — bulk upsert via `Repo.insert_all` with `on_conflict: :replace` - `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 ## 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: 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 ## 7. Scheduled sync worker
**New file:** `lib/simpleshop_theme/sync/scheduled_sync_worker.ex` **New file:** `lib/berrypod/sync/scheduled_sync_worker.ex`
```elixir ```elixir
use Oban.Worker, queue: :sync, max_attempts: 1 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`): **Oban cron config** (`config/config.exs`):
```elixir ```elixir
{"0 */6 * * *", SimpleshopTheme.Sync.ScheduledSyncWorker} {"0 */6 * * *", Berrypod.Sync.ScheduledSyncWorker}
``` ```
Every 6 hours. `:sync` queue concurrency 1 serialises with manual syncs. Every 6 hours. `:sync` queue concurrency 1 serialises with manual syncs.
## 8. Checkout: Stripe shipping_options ## 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`: 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). 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`. On `checkout.session.completed`, extract `session.shipping_cost.amount_total` and update order's `shipping_cost` and `total`.
## 9. Country detection plug ## 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: Simple plug that runs on the `:browser` pipeline:
1. Check session for existing `country_code` — if present, skip 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 ## 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)`. 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`: Update `order_summary` component — add `attr :shipping_estimate, :integer, default: nil`:
- Non-nil → show `"From #{format_price(shipping_estimate)}"` + total includes estimate - 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/..._create_shipping_rates.exs` | New migration |
| `priv/repo/migrations/..._add_shipping_cost_to_orders.exs` | New migration | | `priv/repo/migrations/..._add_shipping_cost_to_orders.exs` | New migration |
| `lib/simpleshop_theme/shipping/shipping_rate.ex` | New schema | | `lib/berrypod/shipping/shipping_rate.ex` | New schema |
| `lib/simpleshop_theme/shipping.ex` | New context | | `lib/berrypod/shipping.ex` | New context |
| `lib/simpleshop_theme/providers/provider.ex` | Add optional callback | | `lib/berrypod/providers/provider.ex` | Add optional callback |
| `lib/simpleshop_theme/providers/printify.ex` | Implement `fetch_shipping_rates/2` | | `lib/berrypod/providers/printify.ex` | Implement `fetch_shipping_rates/2` |
| `lib/simpleshop_theme/sync/product_sync_worker.ex` | Wire shipping into sync | | `lib/berrypod/sync/product_sync_worker.ex` | Wire shipping into sync |
| `lib/simpleshop_theme/sync/scheduled_sync_worker.ex` | New Oban cron worker | | `lib/berrypod/sync/scheduled_sync_worker.ex` | New Oban cron worker |
| `config/config.exs` | Add ScheduledSyncWorker to crontab | | `config/config.exs` | Add ScheduledSyncWorker to crontab |
| `lib/simpleshop_theme/orders/order.ex` | Add `shipping_cost` field | | `lib/berrypod/orders/order.ex` | Add `shipping_cost` field |
| `lib/simpleshop_theme/orders.ex` | Cast shipping_cost, update total logic | | `lib/berrypod/orders.ex` | Cast shipping_cost, update total logic |
| `lib/simpleshop_theme/products.ex` | Add `get_variants_with_products/1` | | `lib/berrypod/products.ex` | Add `get_variants_with_products/1` |
| `lib/simpleshop_theme_web/controllers/checkout_controller.ex` | Stripe shipping_options | | `lib/berrypod_web/controllers/checkout_controller.ex` | Stripe shipping_options |
| `lib/simpleshop_theme_web/controllers/stripe_webhook_controller.ex` | Extract shipping from Stripe | | `lib/berrypod_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/berrypod_web/plugs/country_detect.ex` | New plug: country from Accept-Language |
| `lib/simpleshop_theme_web/router.ex` | Add CountryDetect to `:browser` pipeline | | `lib/berrypod_web/router.ex` | Add CountryDetect to `:browser` pipeline |
| `lib/simpleshop_theme_web/live/shop/cart.ex` | Shipping estimate assign + country from session | | `lib/berrypod_web/live/shop/cart.ex` | Shipping estimate assign + country from session |
| `lib/simpleshop_theme_web/components/shop_components/cart.ex` | Display estimate | | `lib/berrypod_web/components/shop_components/cart.ex` | Display estimate |
| `lib/simpleshop_theme_web/components/page_templates/cart.html.heex` | Pass shipping_estimate | | `lib/berrypod_web/components/page_templates/cart.html.heex` | Pass shipping_estimate |
**New files:** 5 (schema, context, worker, plug, 2 migrations) **New files:** 5 (schema, context, worker, plug, 2 migrations)
**Modified files:** 14 **Modified files:** 14

View File

@ -1,4 +1,4 @@
# SimpleshopTheme Design Philosophy # Berrypod Design Philosophy
This document captures the design principles and research that informed the theme system architecture. This document captures the design principles and research that informed the theme system architecture.

View File

@ -1,20 +1,20 @@
# fly.toml app configuration file generated for simpleshop-theme on 2026-02-08T20:51:20Z # fly.toml app configuration file generated for berrypod on 2026-02-08T20:51:20Z
# #
# See https://fly.io/docs/reference/configuration/ for information about how to use this file. # See https://fly.io/docs/reference/configuration/ for information about how to use this file.
# #
app = 'simpleshop-theme' app = 'berrypod'
primary_region = 'lhr' primary_region = 'lhr'
[build] [build]
dockerfile = 'Dockerfile' dockerfile = 'Dockerfile'
[env] [env]
DATABASE_PATH = '/data/simpleshop_theme.db' DATABASE_PATH = '/data/berrypod.db'
PHX_SERVER = 'true' PHX_SERVER = 'true'
[[mounts]] [[mounts]]
source = 'simpleshop_data' source = 'berrypod_data'
destination = '/data' destination = '/data'
[http_service] [http_service]

View File

@ -1,6 +1,6 @@
defmodule SimpleshopTheme do defmodule Berrypod do
@moduledoc """ @moduledoc """
SimpleshopTheme keeps the contexts that define your domain Berrypod keeps the contexts that define your domain
and business logic. and business logic.
Contexts are also responsible for managing your data, regardless Contexts are also responsible for managing your data, regardless

View File

@ -1,12 +1,12 @@
defmodule SimpleshopTheme.Accounts do defmodule Berrypod.Accounts do
@moduledoc """ @moduledoc """
The Accounts context. The Accounts context.
""" """
import Ecto.Query, warn: false import Ecto.Query, warn: false
alias SimpleshopTheme.Repo alias Berrypod.Repo
alias SimpleshopTheme.Accounts.{User, UserToken, UserNotifier} alias Berrypod.Accounts.{User, UserToken, UserNotifier}
## Database getters ## Database getters
@ -63,7 +63,7 @@ defmodule SimpleshopTheme.Accounts do
@doc """ @doc """
Returns whether an admin user exists. Returns whether an admin user exists.
SimpleShop is single-tenant any user in the database is the admin. Berrypod is single-tenant any user in the database is the admin.
""" """
def has_admin? do def has_admin? do
Repo.exists?(User) Repo.exists?(User)
@ -108,7 +108,7 @@ defmodule SimpleshopTheme.Accounts do
@doc """ @doc """
Returns an `%Ecto.Changeset{}` for changing the user email. Returns an `%Ecto.Changeset{}` for changing the user email.
See `SimpleshopTheme.Accounts.User.email_changeset/3` for a list of supported options. See `Berrypod.Accounts.User.email_changeset/3` for a list of supported options.
## Examples ## Examples
@ -144,7 +144,7 @@ defmodule SimpleshopTheme.Accounts do
@doc """ @doc """
Returns an `%Ecto.Changeset{}` for changing the user password. Returns an `%Ecto.Changeset{}` for changing the user password.
See `SimpleshopTheme.Accounts.User.password_changeset/3` for a list of supported options. See `Berrypod.Accounts.User.password_changeset/3` for a list of supported options.
## Examples ## Examples

View File

@ -1,8 +1,8 @@
defmodule SimpleshopTheme.Accounts.Scope do defmodule Berrypod.Accounts.Scope do
@moduledoc """ @moduledoc """
Defines the scope of the caller to be used throughout the app. Defines the scope of the caller to be used throughout the app.
The `SimpleshopTheme.Accounts.Scope` allows public interfaces to receive The `Berrypod.Accounts.Scope` allows public interfaces to receive
information about the caller, such as if the call is initiated from an information about the caller, such as if the call is initiated from an
end-user, and if so, which user. Additionally, such a scope can carry fields end-user, and if so, which user. Additionally, such a scope can carry fields
such as "super user" or other privileges for use as authorization, or to such as "super user" or other privileges for use as authorization, or to
@ -16,7 +16,7 @@ defmodule SimpleshopTheme.Accounts.Scope do
growing application requirements. growing application requirements.
""" """
alias SimpleshopTheme.Accounts.User alias Berrypod.Accounts.User
defstruct user: nil defstruct user: nil

View File

@ -1,4 +1,4 @@
defmodule SimpleshopTheme.Accounts.User do defmodule Berrypod.Accounts.User do
use Ecto.Schema use Ecto.Schema
import Ecto.Changeset import Ecto.Changeset
@ -42,7 +42,7 @@ defmodule SimpleshopTheme.Accounts.User do
if Keyword.get(opts, :validate_unique, true) do if Keyword.get(opts, :validate_unique, true) do
changeset changeset
|> unsafe_validate_unique(:email, SimpleshopTheme.Repo) |> unsafe_validate_unique(:email, Berrypod.Repo)
|> unique_constraint(:email) |> unique_constraint(:email)
|> validate_email_changed() |> validate_email_changed()
else else
@ -122,7 +122,7 @@ defmodule SimpleshopTheme.Accounts.User do
If there is no user or the user doesn't have a password, we call If there is no user or the user doesn't have a password, we call
`Bcrypt.no_user_verify/0` to avoid timing attacks. `Bcrypt.no_user_verify/0` to avoid timing attacks.
""" """
def valid_password?(%SimpleshopTheme.Accounts.User{hashed_password: hashed_password}, password) def valid_password?(%Berrypod.Accounts.User{hashed_password: hashed_password}, password)
when is_binary(hashed_password) and byte_size(password) > 0 do when is_binary(hashed_password) and byte_size(password) > 0 do
Bcrypt.verify_pass(password, hashed_password) Bcrypt.verify_pass(password, hashed_password)
end end

View File

@ -1,15 +1,15 @@
defmodule SimpleshopTheme.Accounts.UserNotifier do defmodule Berrypod.Accounts.UserNotifier do
import Swoosh.Email import Swoosh.Email
alias SimpleshopTheme.Mailer alias Berrypod.Mailer
alias SimpleshopTheme.Accounts.User alias Berrypod.Accounts.User
# Delivers the email using the application mailer. # Delivers the email using the application mailer.
defp deliver(recipient, subject, body) do defp deliver(recipient, subject, body) do
email = email =
new() new()
|> to(recipient) |> to(recipient)
|> from({"SimpleshopTheme", "contact@example.com"}) |> from({"Berrypod", "contact@example.com"})
|> subject(subject) |> subject(subject)
|> text_body(body) |> text_body(body)

View File

@ -1,7 +1,7 @@
defmodule SimpleshopTheme.Accounts.UserToken do defmodule Berrypod.Accounts.UserToken do
use Ecto.Schema use Ecto.Schema
import Ecto.Query import Ecto.Query
alias SimpleshopTheme.Accounts.UserToken alias Berrypod.Accounts.UserToken
@hash_algorithm :sha256 @hash_algorithm :sha256
@rand_size 32 @rand_size 32
@ -19,7 +19,7 @@ defmodule SimpleshopTheme.Accounts.UserToken do
field :context, :string field :context, :string
field :sent_to, :string field :sent_to, :string
field :authenticated_at, :utc_datetime field :authenticated_at, :utc_datetime
belongs_to :user, SimpleshopTheme.Accounts.User belongs_to :user, Berrypod.Accounts.User
timestamps(type: :utc_datetime, updated_at: false) timestamps(type: :utc_datetime, updated_at: false)
end end

View File

@ -1,4 +1,4 @@
defmodule SimpleshopTheme.Application do defmodule Berrypod.Application do
# See https://hexdocs.pm/elixir/Application.html # See https://hexdocs.pm/elixir/Application.html
# for more information on OTP Applications # for more information on OTP Applications
@moduledoc false @moduledoc false
@ -8,29 +8,29 @@ defmodule SimpleshopTheme.Application do
@impl true @impl true
def start(_type, _args) do def start(_type, _args) do
children = [ children = [
SimpleshopThemeWeb.Telemetry, BerrypodWeb.Telemetry,
SimpleshopTheme.Repo, Berrypod.Repo,
{Ecto.Migrator, {Ecto.Migrator,
repos: Application.fetch_env!(:simpleshop_theme, :ecto_repos), skip: skip_migrations?()}, repos: Application.fetch_env!(:berrypod, :ecto_repos), skip: skip_migrations?()},
# Seed default theme settings if none exist (first boot) # Seed default theme settings if none exist (first boot)
Supervisor.child_spec({Task, &SimpleshopTheme.Release.seed_defaults/0}, id: :seed_defaults), Supervisor.child_spec({Task, &Berrypod.Release.seed_defaults/0}, id: :seed_defaults),
# Load encrypted secrets from DB into Application env # Load encrypted secrets from DB into Application env
{Task, &SimpleshopTheme.Secrets.load_all/0}, {Task, &Berrypod.Secrets.load_all/0},
{DNSCluster, query: Application.get_env(:simpleshop_theme, :dns_cluster_query) || :ignore}, {DNSCluster, query: Application.get_env(:berrypod, :dns_cluster_query) || :ignore},
{Phoenix.PubSub, name: SimpleshopTheme.PubSub}, {Phoenix.PubSub, name: Berrypod.PubSub},
# Background job processing # Background job processing
{Oban, Application.fetch_env!(:simpleshop_theme, Oban)}, {Oban, Application.fetch_env!(:berrypod, Oban)},
# Image variant cache - ensures all variants exist on startup # Image variant cache - ensures all variants exist on startup
SimpleshopTheme.Images.VariantCache, Berrypod.Images.VariantCache,
# Start to serve requests # Start to serve requests
SimpleshopThemeWeb.Endpoint, BerrypodWeb.Endpoint,
# Theme CSS cache - must start after Endpoint for static_path/1 to work # Theme CSS cache - must start after Endpoint for static_path/1 to work
SimpleshopTheme.Theme.CSSCache Berrypod.Theme.CSSCache
] ]
# See https://hexdocs.pm/elixir/Supervisor.html # See https://hexdocs.pm/elixir/Supervisor.html
# for other strategies and supported options # for other strategies and supported options
opts = [strategy: :one_for_one, name: SimpleshopTheme.Supervisor] opts = [strategy: :one_for_one, name: Berrypod.Supervisor]
Supervisor.start_link(children, opts) Supervisor.start_link(children, opts)
end end
@ -38,7 +38,7 @@ defmodule SimpleshopTheme.Application do
# whenever the application is updated. # whenever the application is updated.
@impl true @impl true
def config_change(changed, _new, removed) do def config_change(changed, _new, removed) do
SimpleshopThemeWeb.Endpoint.config_change(changed, removed) BerrypodWeb.Endpoint.config_change(changed, removed)
:ok :ok
end end

View File

@ -1,4 +1,4 @@
defmodule SimpleshopTheme.Cart do defmodule Berrypod.Cart do
@moduledoc """ @moduledoc """
The Cart context. The Cart context.
@ -7,8 +7,8 @@ defmodule SimpleshopTheme.Cart do
Items are hydrated with full product/variant data when needed for display. Items are hydrated with full product/variant data when needed for display.
""" """
alias SimpleshopTheme.Products alias Berrypod.Products
alias SimpleshopTheme.Products.ProductImage alias Berrypod.Products.ProductImage
@session_key "cart" @session_key "cart"

View File

@ -1,4 +1,4 @@
defmodule SimpleshopTheme.Cldr do defmodule Berrypod.Cldr do
@moduledoc """ @moduledoc """
CLDR backend for internationalization and currency formatting. CLDR backend for internationalization and currency formatting.

View File

@ -1,4 +1,4 @@
defmodule SimpleshopTheme.Clients.Printful do defmodule Berrypod.Clients.Printful do
@moduledoc """ @moduledoc """
HTTP client for the Printful API. HTTP client for the Printful API.

View File

@ -1,4 +1,4 @@
defmodule SimpleshopTheme.Clients.Printify do defmodule Berrypod.Clients.Printify do
@moduledoc """ @moduledoc """
HTTP client for the Printify API. HTTP client for the Printify API.

View File

@ -1,4 +1,4 @@
defmodule SimpleshopTheme.ExchangeRate do defmodule Berrypod.ExchangeRate do
@moduledoc """ @moduledoc """
Fetches and caches exchange rates for shipping cost conversion. Fetches and caches exchange rates for shipping cost conversion.
@ -7,7 +7,7 @@ defmodule SimpleshopTheme.ExchangeRate do
they survive restarts without an API call. they survive restarts without an API call.
""" """
alias SimpleshopTheme.Settings alias Berrypod.Settings
require Logger require Logger

View File

@ -1,11 +1,11 @@
defmodule SimpleshopTheme.Images.OptimizeWorker do defmodule Berrypod.Images.OptimizeWorker do
@moduledoc """ @moduledoc """
Oban worker for processing image variants in the background. Oban worker for processing image variants in the background.
Handles both database images and filesystem mockups. Handles both database images and filesystem mockups.
""" """
use Oban.Worker, queue: :images, max_attempts: 3 use Oban.Worker, queue: :images, max_attempts: 3
alias SimpleshopTheme.Images.Optimizer alias Berrypod.Images.Optimizer
@impl Oban.Worker @impl Oban.Worker
def perform(%Oban.Job{args: %{"type" => "mockup", "source_path" => source_path}}) do def perform(%Oban.Job{args: %{"type" => "mockup", "source_path" => source_path}}) do

View File

@ -1,12 +1,12 @@
defmodule SimpleshopTheme.Images.Optimizer do defmodule Berrypod.Images.Optimizer do
@moduledoc """ @moduledoc """
Generates optimized image variants. Only creates sizes source dimensions. Generates optimized image variants. Only creates sizes source dimensions.
""" """
require Logger require Logger
alias SimpleshopTheme.Repo alias Berrypod.Repo
alias SimpleshopTheme.Media.Image, as: ImageSchema alias Berrypod.Media.Image, as: ImageSchema
@all_widths [400, 800, 1200] @all_widths [400, 800, 1200]
@pregenerated_formats [:avif, :webp, :jpg] @pregenerated_formats [:avif, :webp, :jpg]
@ -15,8 +15,8 @@ defmodule SimpleshopTheme.Images.Optimizer do
@storage_quality 90 @storage_quality 90
def cache_dir do def cache_dir do
Application.get_env(:simpleshop_theme, :image_cache_dir) || Application.get_env(:berrypod, :image_cache_dir) ||
Application.app_dir(:simpleshop_theme, "priv/static/image_cache") Application.app_dir(:berrypod, "priv/static/image_cache")
end end
def all_widths, do: @all_widths def all_widths, do: @all_widths

View File

@ -1,4 +1,4 @@
defmodule SimpleshopTheme.Images.VariantCache do defmodule Berrypod.Images.VariantCache do
@moduledoc """ @moduledoc """
Ensures all image variants exist on startup. Ensures all image variants exist on startup.
@ -12,14 +12,14 @@ defmodule SimpleshopTheme.Images.VariantCache do
use GenServer use GenServer
require Logger require Logger
alias SimpleshopTheme.Repo alias Berrypod.Repo
alias SimpleshopTheme.Media.Image, as: ImageSchema alias Berrypod.Media.Image, as: ImageSchema
alias SimpleshopTheme.Images.{Optimizer, OptimizeWorker} alias Berrypod.Images.{Optimizer, OptimizeWorker}
alias SimpleshopTheme.Products alias Berrypod.Products
alias SimpleshopTheme.Sync.ImageDownloadWorker alias Berrypod.Sync.ImageDownloadWorker
import Ecto.Query import Ecto.Query
defp mockup_dir, do: Application.app_dir(:simpleshop_theme, "priv/static/mockups") defp mockup_dir, do: Application.app_dir(:berrypod, "priv/static/mockups")
def start_link(opts) do def start_link(opts) do
GenServer.start_link(__MODULE__, opts, name: __MODULE__) GenServer.start_link(__MODULE__, opts, name: __MODULE__)

3
lib/berrypod/mailer.ex Normal file
View File

@ -0,0 +1,3 @@
defmodule Berrypod.Mailer do
use Swoosh.Mailer, otp_app: :berrypod
end

View File

@ -1,13 +1,13 @@
defmodule SimpleshopTheme.Media do defmodule Berrypod.Media do
@moduledoc """ @moduledoc """
The Media context for managing images and file uploads. The Media context for managing images and file uploads.
""" """
import Ecto.Query, warn: false import Ecto.Query, warn: false
alias SimpleshopTheme.Repo alias Berrypod.Repo
alias SimpleshopTheme.Media.Image, as: ImageSchema alias Berrypod.Media.Image, as: ImageSchema
alias SimpleshopTheme.Images.Optimizer alias Berrypod.Images.Optimizer
alias SimpleshopTheme.Images.OptimizeWorker alias Berrypod.Images.OptimizeWorker
@doc """ @doc """
Uploads an image and stores it in the database. Uploads an image and stores it in the database.

View File

@ -1,4 +1,4 @@
defmodule SimpleshopTheme.Media.Image do defmodule Berrypod.Media.Image do
use Ecto.Schema use Ecto.Schema
import Ecto.Changeset import Ecto.Changeset

View File

@ -1,4 +1,4 @@
defmodule SimpleshopTheme.Media.SVGRecolorer do defmodule Berrypod.Media.SVGRecolorer do
@moduledoc """ @moduledoc """
Recolors SVG images by replacing fill and stroke colors with a target color. Recolors SVG images by replacing fill and stroke colors with a target color.

View File

@ -1,4 +1,4 @@
defmodule SimpleshopTheme.Mockups.Generator do defmodule Berrypod.Mockups.Generator do
@moduledoc """ @moduledoc """
Generates product mockups using the Printify API. Generates product mockups using the Printify API.
@ -11,7 +11,7 @@ defmodule SimpleshopTheme.Mockups.Generator do
6. Optionally cleaning up created products 6. Optionally cleaning up created products
""" """
alias SimpleshopTheme.Clients.Printify, as: Client alias Berrypod.Clients.Printify, as: Client
@output_dir "priv/static/mockups" @output_dir "priv/static/mockups"
@ -382,7 +382,7 @@ defmodule SimpleshopTheme.Mockups.Generator do
Sources are saved for regeneration on startup via VariantCache. Sources are saved for regeneration on startup via VariantCache.
""" """
def download_mockups(product_slug, mockup_urls) do def download_mockups(product_slug, mockup_urls) do
alias SimpleshopTheme.Images.Optimizer alias Berrypod.Images.Optimizer
File.mkdir_p!(@output_dir) File.mkdir_p!(@output_dir)

View File

@ -1,4 +1,4 @@
defmodule SimpleshopTheme.Mockups.PrintfulGenerator do defmodule Berrypod.Mockups.PrintfulGenerator do
@moduledoc """ @moduledoc """
Generates product mockups and/or creates demo products using the Printful API. Generates product mockups and/or creates demo products using the Printful API.
@ -11,9 +11,9 @@ defmodule SimpleshopTheme.Mockups.PrintfulGenerator do
v1 store products API. These can later be synced into the shop. v1 store products API. These can later be synced into the shop.
""" """
alias SimpleshopTheme.Clients.Printful, as: Client alias Berrypod.Clients.Printful, as: Client
alias SimpleshopTheme.Images.Optimizer alias Berrypod.Images.Optimizer
alias SimpleshopTheme.Mockups.Generator alias Berrypod.Mockups.Generator
@output_dir "priv/static/mockups" @output_dir "priv/static/mockups"
@poll_interval_ms 2_000 @poll_interval_ms 2_000

View File

@ -1,4 +1,4 @@
defmodule SimpleshopTheme.Orders do defmodule Berrypod.Orders do
@moduledoc """ @moduledoc """
The Orders context. The Orders context.
@ -8,10 +8,10 @@ defmodule SimpleshopTheme.Orders do
""" """
import Ecto.Query import Ecto.Query
alias SimpleshopTheme.Repo alias Berrypod.Repo
alias SimpleshopTheme.Orders.{Order, OrderItem, OrderNotifier} alias Berrypod.Orders.{Order, OrderItem, OrderNotifier}
alias SimpleshopTheme.Products alias Berrypod.Products
alias SimpleshopTheme.Providers.Provider alias Berrypod.Providers.Provider
require Logger require Logger

View File

@ -1,4 +1,4 @@
defmodule SimpleshopTheme.Orders.FulfilmentStatusWorker do defmodule Berrypod.Orders.FulfilmentStatusWorker do
@moduledoc """ @moduledoc """
Oban Cron worker that polls the fulfilment provider for status updates. Oban Cron worker that polls the fulfilment provider for status updates.
@ -9,7 +9,7 @@ defmodule SimpleshopTheme.Orders.FulfilmentStatusWorker do
use Oban.Worker, queue: :sync, max_attempts: 1 use Oban.Worker, queue: :sync, max_attempts: 1
alias SimpleshopTheme.Orders alias Berrypod.Orders
require Logger require Logger

View File

@ -1,4 +1,4 @@
defmodule SimpleshopTheme.Orders.Order do defmodule Berrypod.Orders.Order do
use Ecto.Schema use Ecto.Schema
import Ecto.Changeset import Ecto.Changeset
@ -34,7 +34,7 @@ defmodule SimpleshopTheme.Orders.Order do
field :shipped_at, :utc_datetime field :shipped_at, :utc_datetime
field :delivered_at, :utc_datetime field :delivered_at, :utc_datetime
has_many :items, SimpleshopTheme.Orders.OrderItem has_many :items, Berrypod.Orders.OrderItem
timestamps(type: :utc_datetime) timestamps(type: :utc_datetime)
end end

View File

@ -1,4 +1,4 @@
defmodule SimpleshopTheme.Orders.OrderItem do defmodule Berrypod.Orders.OrderItem do
use Ecto.Schema use Ecto.Schema
import Ecto.Changeset import Ecto.Changeset
@ -12,7 +12,7 @@ defmodule SimpleshopTheme.Orders.OrderItem do
field :quantity, :integer field :quantity, :integer
field :unit_price, :integer field :unit_price, :integer
belongs_to :order, SimpleshopTheme.Orders.Order belongs_to :order, Berrypod.Orders.Order
timestamps(type: :utc_datetime) timestamps(type: :utc_datetime)
end end

View File

@ -1,4 +1,4 @@
defmodule SimpleshopTheme.Orders.OrderNotifier do defmodule Berrypod.Orders.OrderNotifier do
@moduledoc """ @moduledoc """
Sends transactional emails for orders. Sends transactional emails for orders.
@ -7,8 +7,8 @@ defmodule SimpleshopTheme.Orders.OrderNotifier do
import Swoosh.Email import Swoosh.Email
alias SimpleshopTheme.Cart alias Berrypod.Cart
alias SimpleshopTheme.Mailer alias Berrypod.Mailer
require Logger require Logger
@ -73,7 +73,7 @@ defmodule SimpleshopTheme.Orders.OrderNotifier do
email = email =
new() new()
|> to(recipient) |> to(recipient)
|> from({"SimpleshopTheme", "contact@example.com"}) |> from({"Berrypod", "contact@example.com"})
|> subject(subject) |> subject(subject)
|> text_body(body) |> text_body(body)

View File

@ -1,4 +1,4 @@
defmodule SimpleshopTheme.Orders.OrderSubmissionWorker do defmodule Berrypod.Orders.OrderSubmissionWorker do
@moduledoc """ @moduledoc """
Oban worker for submitting paid orders to the fulfilment provider. Oban worker for submitting paid orders to the fulfilment provider.
@ -9,7 +9,7 @@ defmodule SimpleshopTheme.Orders.OrderSubmissionWorker do
use Oban.Worker, queue: :checkout, max_attempts: 3 use Oban.Worker, queue: :checkout, max_attempts: 3
alias SimpleshopTheme.Orders alias Berrypod.Orders
require Logger require Logger

View File

@ -1,4 +1,4 @@
defmodule SimpleshopTheme.Products do defmodule Berrypod.Products do
@moduledoc """ @moduledoc """
The Products context. The Products context.
@ -7,8 +7,8 @@ defmodule SimpleshopTheme.Products do
""" """
import Ecto.Query import Ecto.Query
alias SimpleshopTheme.Repo alias Berrypod.Repo
alias SimpleshopTheme.Products.{ProviderConnection, Product, ProductImage, ProductVariant} alias Berrypod.Products.{ProviderConnection, Product, ProductImage, ProductVariant}
# ============================================================================= # =============================================================================
# Provider Connections # Provider Connections
@ -105,7 +105,7 @@ defmodule SimpleshopTheme.Products do
Returns `{:ok, job}` or `{:error, changeset}`. Returns `{:ok, job}` or `{:error, changeset}`.
""" """
def enqueue_sync(%ProviderConnection{} = conn) do def enqueue_sync(%ProviderConnection{} = conn) do
SimpleshopTheme.Sync.ProductSyncWorker.enqueue(conn.id) Berrypod.Sync.ProductSyncWorker.enqueue(conn.id)
end end
# ============================================================================= # =============================================================================
@ -669,7 +669,7 @@ defmodule SimpleshopTheme.Products do
defp cleanup_orphaned_images([]), do: :ok defp cleanup_orphaned_images([]), do: :ok
defp cleanup_orphaned_images(image_ids) do defp cleanup_orphaned_images(image_ids) do
alias SimpleshopTheme.Media.Image, as: ImageSchema alias Berrypod.Media.Image, as: ImageSchema
from(i in ImageSchema, where: i.id in ^image_ids) from(i in ImageSchema, where: i.id in ^image_ids)
|> Repo.delete_all() |> Repo.delete_all()

View File

@ -1,4 +1,4 @@
defmodule SimpleshopTheme.Products.Product do defmodule Berrypod.Products.Product do
@moduledoc """ @moduledoc """
Schema for products synced from POD providers. Schema for products synced from POD providers.
@ -31,9 +31,9 @@ defmodule SimpleshopTheme.Products.Product do
field :in_stock, :boolean, default: true field :in_stock, :boolean, default: true
field :on_sale, :boolean, default: false field :on_sale, :boolean, default: false
belongs_to :provider_connection, SimpleshopTheme.Products.ProviderConnection belongs_to :provider_connection, Berrypod.Products.ProviderConnection
has_many :images, SimpleshopTheme.Products.ProductImage has_many :images, Berrypod.Products.ProductImage
has_many :variants, SimpleshopTheme.Products.ProductVariant has_many :variants, Berrypod.Products.ProductVariant
timestamps(type: :utc_datetime) timestamps(type: :utc_datetime)
end end

View File

@ -1,4 +1,4 @@
defmodule SimpleshopTheme.Products.ProductImage do defmodule Berrypod.Products.ProductImage do
@moduledoc """ @moduledoc """
Schema for product images. Schema for product images.
@ -18,8 +18,8 @@ defmodule SimpleshopTheme.Products.ProductImage do
field :color, :string field :color, :string
field :image_id, :binary_id field :image_id, :binary_id
belongs_to :product, SimpleshopTheme.Products.Product belongs_to :product, Berrypod.Products.Product
belongs_to :image, SimpleshopTheme.Media.Image, define_field: false belongs_to :image, Berrypod.Media.Image, define_field: false
timestamps(type: :utc_datetime) timestamps(type: :utc_datetime)
end end

View File

@ -1,4 +1,4 @@
defmodule SimpleshopTheme.Products.ProductVariant do defmodule Berrypod.Products.ProductVariant do
@moduledoc """ @moduledoc """
Schema for product variants. Schema for product variants.
@ -34,7 +34,7 @@ defmodule SimpleshopTheme.Products.ProductVariant do
field :is_enabled, :boolean, default: true field :is_enabled, :boolean, default: true
field :is_available, :boolean, default: true field :is_available, :boolean, default: true
belongs_to :product, SimpleshopTheme.Products.Product belongs_to :product, Berrypod.Products.Product
timestamps(type: :utc_datetime) timestamps(type: :utc_datetime)
end end

View File

@ -1,4 +1,4 @@
defmodule SimpleshopTheme.Products.ProviderConnection do defmodule Berrypod.Products.ProviderConnection do
@moduledoc """ @moduledoc """
Schema for POD provider connections. Schema for POD provider connections.
@ -9,7 +9,7 @@ defmodule SimpleshopTheme.Products.ProviderConnection do
use Ecto.Schema use Ecto.Schema
import Ecto.Changeset import Ecto.Changeset
alias SimpleshopTheme.Vault alias Berrypod.Vault
@primary_key {:id, :binary_id, autogenerate: true} @primary_key {:id, :binary_id, autogenerate: true}
@foreign_key_type :binary_id @foreign_key_type :binary_id
@ -29,7 +29,7 @@ defmodule SimpleshopTheme.Products.ProviderConnection do
# Virtual field for setting API key # Virtual field for setting API key
field :api_key, :string, virtual: true field :api_key, :string, virtual: true
has_many :products, SimpleshopTheme.Products.Product has_many :products, Berrypod.Products.Product
timestamps(type: :utc_datetime) timestamps(type: :utc_datetime)
end end

View File

@ -1,10 +1,10 @@
defmodule SimpleshopTheme.Providers do defmodule Berrypod.Providers do
@moduledoc """ @moduledoc """
Convenience functions for working with POD providers. Convenience functions for working with POD providers.
""" """
alias SimpleshopTheme.Products.ProviderConnection alias Berrypod.Products.ProviderConnection
alias SimpleshopTheme.Providers.Provider alias Berrypod.Providers.Provider
@doc """ @doc """
Tests a provider connection. Tests a provider connection.

View File

@ -1,4 +1,4 @@
defmodule SimpleshopTheme.Providers.Printful do defmodule Berrypod.Providers.Printful do
@moduledoc """ @moduledoc """
Printful provider implementation. Printful provider implementation.
@ -6,10 +6,10 @@ defmodule SimpleshopTheme.Providers.Printful do
Uses v2 API endpoints where available, v1 for sync products. Uses v2 API endpoints where available, v1 for sync products.
""" """
@behaviour SimpleshopTheme.Providers.Provider @behaviour Berrypod.Providers.Provider
alias SimpleshopTheme.Clients.Printful, as: Client alias Berrypod.Clients.Printful, as: Client
alias SimpleshopTheme.Products.ProviderConnection alias Berrypod.Products.ProviderConnection
require Logger require Logger

View File

@ -1,14 +1,14 @@
defmodule SimpleshopTheme.Providers.Printify do defmodule Berrypod.Providers.Printify do
@moduledoc """ @moduledoc """
Printify provider implementation. Printify provider implementation.
Handles product sync and order submission for Printify. Handles product sync and order submission for Printify.
""" """
@behaviour SimpleshopTheme.Providers.Provider @behaviour Berrypod.Providers.Provider
alias SimpleshopTheme.Clients.Printify, as: Client alias Berrypod.Clients.Printify, as: Client
alias SimpleshopTheme.Products.ProviderConnection alias Berrypod.Products.ProviderConnection
require Logger require Logger

View File

@ -1,4 +1,4 @@
defmodule SimpleshopTheme.Providers.Provider do defmodule Berrypod.Providers.Provider do
@moduledoc """ @moduledoc """
Behaviour for POD provider integrations. Behaviour for POD provider integrations.
@ -21,7 +21,7 @@ defmodule SimpleshopTheme.Providers.Provider do
- Images are maps with keys: `src`, `position`, `alt` - Images are maps with keys: `src`, `position`, `alt`
""" """
alias SimpleshopTheme.Products.ProviderConnection alias Berrypod.Products.ProviderConnection
@doc """ @doc """
Returns the provider type identifier (e.g., "printify", "gelato"). Returns the provider type identifier (e.g., "printify", "gelato").
@ -79,7 +79,7 @@ defmodule SimpleshopTheme.Providers.Provider do
overrides via Mox. Falls back to hardcoded dispatch. overrides via Mox. Falls back to hardcoded dispatch.
""" """
def for_type(type) do def for_type(type) do
case Application.get_env(:simpleshop_theme, :provider_modules, %{}) do case Application.get_env(:berrypod, :provider_modules, %{}) do
modules when is_map(modules) -> modules when is_map(modules) ->
case Map.get(modules, type) do case Map.get(modules, type) do
nil -> default_for_type(type) nil -> default_for_type(type)
@ -91,10 +91,10 @@ defmodule SimpleshopTheme.Providers.Provider do
end end
end end
defp default_for_type("printify"), do: {:ok, SimpleshopTheme.Providers.Printify} defp default_for_type("printify"), do: {:ok, Berrypod.Providers.Printify}
defp default_for_type("gelato"), do: {:error, :not_implemented} defp default_for_type("gelato"), do: {:error, :not_implemented}
defp default_for_type("prodigi"), do: {:error, :not_implemented} defp default_for_type("prodigi"), do: {:error, :not_implemented}
defp default_for_type("printful"), do: {:ok, SimpleshopTheme.Providers.Printful} defp default_for_type("printful"), do: {:ok, Berrypod.Providers.Printful}
defp default_for_type(type), do: {:error, {:unknown_provider, type}} defp default_for_type(type), do: {:error, {:unknown_provider, type}}
@doc """ @doc """

View File

@ -1,12 +1,12 @@
defmodule SimpleshopTheme.Release do defmodule Berrypod.Release do
@moduledoc """ @moduledoc """
Release tasks that can be run via `bin/migrate` or `bin/simpleshop_theme eval`. Release tasks that can be run via `bin/migrate` or `bin/berrypod eval`.
Migrations run automatically on startup (see Application), so this is mainly Migrations run automatically on startup (see Application), so this is mainly
useful as a standalone tool for debugging or manual recovery. useful as a standalone tool for debugging or manual recovery.
""" """
@app :simpleshop_theme @app :berrypod
def migrate do def migrate do
load_app() load_app()
@ -23,7 +23,7 @@ defmodule SimpleshopTheme.Release do
(when the settings table is empty). Safe to call repeatedly. (when the settings table is empty). Safe to call repeatedly.
""" """
def seed_defaults do def seed_defaults do
alias SimpleshopTheme.Settings alias Berrypod.Settings
case Settings.get_setting("theme_settings") do case Settings.get_setting("theme_settings") do
nil -> nil ->

5
lib/berrypod/repo.ex Normal file
View File

@ -0,0 +1,5 @@
defmodule Berrypod.Repo do
use Ecto.Repo,
otp_app: :berrypod,
adapter: Ecto.Adapters.SQLite3
end

View File

@ -1,4 +1,4 @@
defmodule SimpleshopTheme.Search do defmodule Berrypod.Search do
@moduledoc """ @moduledoc """
Full-text product search backed by SQLite FTS5. Full-text product search backed by SQLite FTS5.
@ -8,8 +8,8 @@ defmodule SimpleshopTheme.Search do
import Ecto.Query import Ecto.Query
alias SimpleshopTheme.Products.Product alias Berrypod.Products.Product
alias SimpleshopTheme.Repo alias Berrypod.Repo
@listing_preloads [images: :image] @listing_preloads [images: :image]

View File

@ -1,4 +1,4 @@
defmodule SimpleshopTheme.Secrets do defmodule Berrypod.Secrets do
@moduledoc """ @moduledoc """
Loads encrypted secrets from the database into Application env at runtime. Loads encrypted secrets from the database into Application env at runtime.
@ -9,7 +9,7 @@ defmodule SimpleshopTheme.Secrets do
The only external dependency is `SECRET_KEY_BASE` (used to derive encryption keys). The only external dependency is `SECRET_KEY_BASE` (used to derive encryption keys).
""" """
alias SimpleshopTheme.Settings alias Berrypod.Settings
require Logger require Logger

View File

@ -1,12 +1,12 @@
defmodule SimpleshopTheme.Settings do defmodule Berrypod.Settings do
@moduledoc """ @moduledoc """
The Settings context for managing site-wide configuration. The Settings context for managing site-wide configuration.
""" """
import Ecto.Query, warn: false import Ecto.Query, warn: false
alias SimpleshopTheme.Repo alias Berrypod.Repo
alias SimpleshopTheme.Settings.{Setting, ThemeSettings} alias Berrypod.Settings.{Setting, ThemeSettings}
alias SimpleshopTheme.Vault alias Berrypod.Vault
@doc """ @doc """
Gets a setting by key with an optional default value. Gets a setting by key with an optional default value.
@ -86,7 +86,7 @@ defmodule SimpleshopTheme.Settings do
put_setting("theme_settings", json, "json") put_setting("theme_settings", json, "json")
# Invalidate and rewarm CSS cache # Invalidate and rewarm CSS cache
alias SimpleshopTheme.Theme.{CSSCache, CSSGenerator} alias Berrypod.Theme.{CSSCache, CSSGenerator}
CSSCache.invalidate() CSSCache.invalidate()
css = CSSGenerator.generate(settings) css = CSSGenerator.generate(settings)
CSSCache.put(css) CSSCache.put(css)
@ -107,7 +107,7 @@ defmodule SimpleshopTheme.Settings do
""" """
def apply_preset(preset_name) when is_atom(preset_name) do def apply_preset(preset_name) when is_atom(preset_name) do
preset = SimpleshopTheme.Theme.Presets.get(preset_name) preset = Berrypod.Theme.Presets.get(preset_name)
if preset do if preset do
update_theme_settings(preset) update_theme_settings(preset)

View File

@ -1,4 +1,4 @@
defmodule SimpleshopTheme.Settings.Setting do defmodule Berrypod.Settings.Setting do
use Ecto.Schema use Ecto.Schema
import Ecto.Changeset import Ecto.Changeset

View File

@ -1,4 +1,4 @@
defmodule SimpleshopTheme.Settings.ThemeSettings do defmodule Berrypod.Settings.ThemeSettings do
use Ecto.Schema use Ecto.Schema
import Ecto.Changeset import Ecto.Changeset

View File

@ -1,9 +1,9 @@
defmodule SimpleshopTheme.Setup do defmodule Berrypod.Setup do
@moduledoc """ @moduledoc """
Aggregates setup status checks for the admin setup flow. Aggregates setup status checks for the admin setup flow.
""" """
alias SimpleshopTheme.{Accounts, Products, Settings} alias Berrypod.{Accounts, Products, Settings}
@doc """ @doc """
Returns a map describing the current setup status. Returns a map describing the current setup status.

View File

@ -1,4 +1,4 @@
defmodule SimpleshopTheme.Shipping do defmodule Berrypod.Shipping do
@moduledoc """ @moduledoc """
The Shipping context. The Shipping context.
@ -7,11 +7,11 @@ defmodule SimpleshopTheme.Shipping do
""" """
import Ecto.Query import Ecto.Query
alias SimpleshopTheme.ExchangeRate alias Berrypod.ExchangeRate
alias SimpleshopTheme.Repo alias Berrypod.Repo
alias SimpleshopTheme.Shipping.ShippingRate alias Berrypod.Shipping.ShippingRate
alias SimpleshopTheme.Products alias Berrypod.Products
alias SimpleshopTheme.Settings alias Berrypod.Settings
require Logger require Logger

View File

@ -1,4 +1,4 @@
defmodule SimpleshopTheme.Shipping.ShippingRate do defmodule Berrypod.Shipping.ShippingRate do
use Ecto.Schema use Ecto.Schema
import Ecto.Changeset import Ecto.Changeset
@ -14,7 +14,7 @@ defmodule SimpleshopTheme.Shipping.ShippingRate do
field :currency, :string, default: "USD" field :currency, :string, default: "USD"
field :handling_time_days, :integer field :handling_time_days, :integer
belongs_to :provider_connection, SimpleshopTheme.Products.ProviderConnection belongs_to :provider_connection, Berrypod.Products.ProviderConnection
timestamps(type: :utc_datetime) timestamps(type: :utc_datetime)
end end

View File

@ -1,11 +1,11 @@
defmodule SimpleshopTheme.Stripe.Setup do defmodule Berrypod.Stripe.Setup do
@moduledoc """ @moduledoc """
Handles Stripe account setup: key verification, automatic webhook Handles Stripe account setup: key verification, automatic webhook
endpoint creation, and teardown. endpoint creation, and teardown.
""" """
alias SimpleshopTheme.Settings alias Berrypod.Settings
alias SimpleshopTheme.Secrets alias Berrypod.Secrets
require Logger require Logger
@ -75,14 +75,14 @@ defmodule SimpleshopTheme.Stripe.Setup do
Returns the webhook URL for this app. Returns the webhook URL for this app.
""" """
def webhook_url do def webhook_url do
"#{SimpleshopThemeWeb.Endpoint.url()}/webhooks/stripe" "#{BerrypodWeb.Endpoint.url()}/webhooks/stripe"
end end
@doc """ @doc """
Returns true if the app is running on localhost (Stripe can't reach it). Returns true if the app is running on localhost (Stripe can't reach it).
""" """
def localhost? do def localhost? do
url = SimpleshopThemeWeb.Endpoint.url() url = BerrypodWeb.Endpoint.url()
uri = URI.parse(url) uri = URI.parse(url)
uri.host in ["localhost", "127.0.0.1", "0.0.0.0", "::1"] uri.host in ["localhost", "127.0.0.1", "0.0.0.0", "::1"]
end end

View File

@ -1,4 +1,4 @@
defmodule SimpleshopTheme.Sync.ImageDownloadWorker do defmodule Berrypod.Sync.ImageDownloadWorker do
@moduledoc """ @moduledoc """
Oban worker for downloading product images from external URLs. Oban worker for downloading product images from external URLs.
@ -17,8 +17,8 @@ defmodule SimpleshopTheme.Sync.ImageDownloadWorker do
use Oban.Worker, queue: :images, max_attempts: 3 use Oban.Worker, queue: :images, max_attempts: 3
alias SimpleshopTheme.Products alias Berrypod.Products
alias SimpleshopTheme.Media alias Berrypod.Media
require Logger require Logger

View File

@ -1,4 +1,4 @@
defmodule SimpleshopTheme.Sync.MockupEnricher do defmodule Berrypod.Sync.MockupEnricher do
@moduledoc """ @moduledoc """
Oban worker that enriches Printful products with extra mockup angle images. Oban worker that enriches Printful products with extra mockup angle images.
@ -16,10 +16,10 @@ defmodule SimpleshopTheme.Sync.MockupEnricher do
use Oban.Worker, queue: :images, max_attempts: 5 use Oban.Worker, queue: :images, max_attempts: 5
alias SimpleshopTheme.Clients.Printful, as: Client alias Berrypod.Clients.Printful, as: Client
alias SimpleshopTheme.Products alias Berrypod.Products
alias SimpleshopTheme.Products.ProviderConnection alias Berrypod.Products.ProviderConnection
alias SimpleshopTheme.Sync.ImageDownloadWorker alias Berrypod.Sync.ImageDownloadWorker
require Logger require Logger

View File

@ -1,4 +1,4 @@
defmodule SimpleshopTheme.Sync.ProductSyncWorker do defmodule Berrypod.Sync.ProductSyncWorker do
@moduledoc """ @moduledoc """
Oban worker for syncing products from POD providers. Oban worker for syncing products from POD providers.
@ -17,11 +17,11 @@ defmodule SimpleshopTheme.Sync.ProductSyncWorker do
use Oban.Worker, queue: :sync, max_attempts: 3 use Oban.Worker, queue: :sync, max_attempts: 3
alias SimpleshopTheme.Products alias Berrypod.Products
alias SimpleshopTheme.Products.ProviderConnection alias Berrypod.Products.ProviderConnection
alias SimpleshopTheme.Providers.Provider alias Berrypod.Providers.Provider
alias SimpleshopTheme.Sync.ImageDownloadWorker alias Berrypod.Sync.ImageDownloadWorker
alias SimpleshopTheme.Sync.MockupEnricher alias Berrypod.Sync.MockupEnricher
require Logger require Logger
@ -110,7 +110,7 @@ defmodule SimpleshopTheme.Sync.ProductSyncWorker do
broadcast_sync(conn.id, {:sync_status, "completed", product_count}) broadcast_sync(conn.id, {:sync_status, "completed", product_count})
# Rebuild search index after successful sync # Rebuild search index after successful sync
SimpleshopTheme.Search.rebuild_index() Berrypod.Search.rebuild_index()
:ok :ok
else else
@ -165,7 +165,7 @@ defmodule SimpleshopTheme.Sync.ProductSyncWorker do
end end
defp broadcast_sync(conn_id, message) do defp broadcast_sync(conn_id, message) do
Phoenix.PubSub.broadcast(SimpleshopTheme.PubSub, "sync:#{conn_id}", message) Phoenix.PubSub.broadcast(Berrypod.PubSub, "sync:#{conn_id}", message)
end end
defp sync_product_associations(product, product_data) do defp sync_product_associations(product, product_data) do
@ -214,11 +214,11 @@ defmodule SimpleshopTheme.Sync.ProductSyncWorker do
defp sync_shipping_rates(conn, provider, products) do defp sync_shipping_rates(conn, provider, products) do
if function_exported?(provider, :fetch_shipping_rates, 2) do if function_exported?(provider, :fetch_shipping_rates, 2) do
# Fetch live exchange rates so shipping costs are stored in GBP # Fetch live exchange rates so shipping costs are stored in GBP
{:ok, exchange_rates} = SimpleshopTheme.ExchangeRate.fetch_and_cache() {:ok, exchange_rates} = Berrypod.ExchangeRate.fetch_and_cache()
case provider.fetch_shipping_rates(conn, products) do case provider.fetch_shipping_rates(conn, products) do
{:ok, rates} when rates != [] -> {:ok, rates} when rates != [] ->
SimpleshopTheme.Shipping.upsert_rates(conn.id, rates, exchange_rates) Berrypod.Shipping.upsert_rates(conn.id, rates, exchange_rates)
{:ok, []} -> {:ok, []} ->
Logger.info("No shipping rates returned for #{conn.provider_type}") Logger.info("No shipping rates returned for #{conn.provider_type}")

View File

@ -1,4 +1,4 @@
defmodule SimpleshopTheme.Sync.ScheduledSyncWorker do defmodule Berrypod.Sync.ScheduledSyncWorker do
@moduledoc """ @moduledoc """
Oban cron worker for periodic product + shipping rate sync. Oban cron worker for periodic product + shipping rate sync.
@ -9,7 +9,7 @@ defmodule SimpleshopTheme.Sync.ScheduledSyncWorker do
use Oban.Worker, queue: :sync, max_attempts: 1 use Oban.Worker, queue: :sync, max_attempts: 1
alias SimpleshopTheme.Products alias Berrypod.Products
require Logger require Logger

View File

@ -1,4 +1,4 @@
defmodule SimpleshopTheme.Theme.CSSCache do defmodule Berrypod.Theme.CSSCache do
@moduledoc """ @moduledoc """
GenServer that maintains an ETS table for caching generated theme CSS. GenServer that maintains an ETS table for caching generated theme CSS.
@ -80,13 +80,13 @@ defmodule SimpleshopTheme.Theme.CSSCache do
""" """
def warm do def warm do
alias SimpleshopTheme.Settings alias Berrypod.Settings
alias SimpleshopTheme.Theme.CSSGenerator alias Berrypod.Theme.CSSGenerator
settings = Settings.get_theme_settings() settings = Settings.get_theme_settings()
# Use endpoint's static_path for digested URLs in production # Use endpoint's static_path for digested URLs in production
path_resolver = &SimpleshopThemeWeb.Endpoint.static_path/1 path_resolver = &BerrypodWeb.Endpoint.static_path/1
css = CSSGenerator.generate(settings, path_resolver) css = CSSGenerator.generate(settings, path_resolver)
put(css) put(css)

View File

@ -1,4 +1,4 @@
defmodule SimpleshopTheme.Theme.CSSGenerator do defmodule Berrypod.Theme.CSSGenerator do
@moduledoc """ @moduledoc """
Generates CSS custom properties (Layer 2: Theme Tokens) from theme settings. Generates CSS custom properties (Layer 2: Theme Tokens) from theme settings.
@ -10,8 +10,8 @@ defmodule SimpleshopTheme.Theme.CSSGenerator do
The theme editor still uses those selectors for live preview switching. The theme editor still uses those selectors for live preview switching.
""" """
alias SimpleshopTheme.Settings.ThemeSettings alias Berrypod.Settings.ThemeSettings
alias SimpleshopTheme.Theme.Fonts alias Berrypod.Theme.Fonts
@doc """ @doc """
Generates CSS for theme settings. Generates CSS for theme settings.
@ -23,7 +23,7 @@ defmodule SimpleshopTheme.Theme.CSSGenerator do
Also includes @font-face declarations for the fonts used by the typography preset. Also includes @font-face declarations for the fonts used by the typography preset.
Accepts an optional path_resolver function for digested font paths. Accepts an optional path_resolver function for digested font paths.
In production, pass `&SimpleshopThemeWeb.Endpoint.static_path/1`. In production, pass `&BerrypodWeb.Endpoint.static_path/1`.
""" """
def generate(%ThemeSettings{} = settings, path_resolver \\ fn path -> path end) do def generate(%ThemeSettings{} = settings, path_resolver \\ fn path -> path end) do
""" """

View File

@ -1,4 +1,4 @@
defmodule SimpleshopTheme.Theme.Fonts do defmodule Berrypod.Theme.Fonts do
@moduledoc """ @moduledoc """
Centralized font configuration for the theme system. Centralized font configuration for the theme system.
@ -116,7 +116,7 @@ defmodule SimpleshopTheme.Theme.Fonts do
Only includes the fonts needed for that preset. Only includes the fonts needed for that preset.
Accepts an optional path_resolver function to transform font URLs. Accepts an optional path_resolver function to transform font URLs.
In production, pass `&SimpleshopThemeWeb.Endpoint.static_path/1` for digested paths. In production, pass `&BerrypodWeb.Endpoint.static_path/1` for digested paths.
""" """
def generate_font_faces(typography, path_resolver \\ &default_path_resolver/1) do def generate_font_faces(typography, path_resolver \\ &default_path_resolver/1) do
%{heading: heading_key, body: body_key} = fonts_for_typography(typography) %{heading: heading_key, body: body_key} = fonts_for_typography(typography)
@ -188,7 +188,7 @@ defmodule SimpleshopTheme.Theme.Fonts do
Returns a list of maps with href, as, type, and crossorigin attributes. Returns a list of maps with href, as, type, and crossorigin attributes.
Accepts an optional path_resolver function for digested paths. Accepts an optional path_resolver function for digested paths.
In production, pass `&SimpleshopThemeWeb.Endpoint.static_path/1`. In production, pass `&BerrypodWeb.Endpoint.static_path/1`.
""" """
def preload_links(typography, path_resolver \\ &default_path_resolver/1) do def preload_links(typography, path_resolver \\ &default_path_resolver/1) do
typography typography

View File

@ -1,6 +1,6 @@
defmodule SimpleshopTheme.Theme.Presets do defmodule Berrypod.Theme.Presets do
@moduledoc """ @moduledoc """
Defines the 8 curated theme presets for SimpleShop. Defines the 8 curated theme presets for Berrypod.
""" """
@presets %{ @presets %{

View File

@ -1,4 +1,4 @@
defmodule SimpleshopTheme.Theme.PreviewData do defmodule Berrypod.Theme.PreviewData do
@moduledoc """ @moduledoc """
Provides preview data for theme customization. Provides preview data for theme customization.
@ -6,7 +6,7 @@ defmodule SimpleshopTheme.Theme.PreviewData do
This allows users to preview themes before adding products to their shop. This allows users to preview themes before adding products to their shop.
""" """
alias SimpleshopTheme.Products alias Berrypod.Products
@doc """ @doc """
Returns products for preview. Returns products for preview.
@ -105,7 +105,7 @@ defmodule SimpleshopTheme.Theme.PreviewData do
%{ %{
type: :lead, type: :lead,
text: text:
"This is a sample about page for your SimpleShop store. You're reading it as Robin, a fictional nature photographer but this is where your own story goes." "This is a sample about page for your Berrypod store. You're reading it as Robin, a fictional nature photographer but this is where your own story goes."
}, },
%{ %{
type: :paragraph, type: :paragraph,

View File

@ -1,4 +1,4 @@
defmodule SimpleshopTheme.Vault do defmodule Berrypod.Vault do
@moduledoc """ @moduledoc """
Handles encryption and decryption of sensitive data. Handles encryption and decryption of sensitive data.
@ -6,7 +6,7 @@ defmodule SimpleshopTheme.Vault do
Keys are derived from the application's secret_key_base. Keys are derived from the application's secret_key_base.
""" """
@aad "SimpleshopTheme.Vault" @aad "Berrypod.Vault"
@doc """ @doc """
Encrypts a string value. Encrypts a string value.
@ -87,7 +87,7 @@ defmodule SimpleshopTheme.Vault do
end end
defp get_secret_key_base do defp get_secret_key_base do
case Application.get_env(:simpleshop_theme, SimpleshopThemeWeb.Endpoint)[:secret_key_base] do case Application.get_env(:berrypod, BerrypodWeb.Endpoint)[:secret_key_base] do
nil -> nil ->
raise """ raise """
Secret key base is not configured. Secret key base is not configured.

View File

@ -1,13 +1,13 @@
defmodule SimpleshopTheme.Webhooks do defmodule Berrypod.Webhooks do
@moduledoc """ @moduledoc """
Handles incoming webhook events from POD providers. Handles incoming webhook events from POD providers.
""" """
alias SimpleshopTheme.Orders alias Berrypod.Orders
alias SimpleshopTheme.Orders.OrderNotifier alias Berrypod.Orders.OrderNotifier
alias SimpleshopTheme.Products alias Berrypod.Products
alias SimpleshopTheme.Sync.ProductSyncWorker alias Berrypod.Sync.ProductSyncWorker
alias SimpleshopTheme.Webhooks.ProductDeleteWorker alias Berrypod.Webhooks.ProductDeleteWorker
require Logger require Logger

View File

@ -1,11 +1,11 @@
defmodule SimpleshopTheme.Webhooks.ProductDeleteWorker do defmodule Berrypod.Webhooks.ProductDeleteWorker do
@moduledoc """ @moduledoc """
Oban worker for deleting products removed from POD providers. Oban worker for deleting products removed from POD providers.
""" """
use Oban.Worker, queue: :sync, max_attempts: 3 use Oban.Worker, queue: :sync, max_attempts: 3
alias SimpleshopTheme.Products alias Berrypod.Products
require Logger require Logger

View File

@ -1,12 +1,12 @@
defmodule SimpleshopThemeWeb do defmodule BerrypodWeb do
@moduledoc """ @moduledoc """
The entrypoint for defining your web interface, such The entrypoint for defining your web interface, such
as controllers, components, channels, and so on. as controllers, components, channels, and so on.
This can be used in your application as: This can be used in your application as:
use SimpleshopThemeWeb, :controller use BerrypodWeb, :controller
use SimpleshopThemeWeb, :html use BerrypodWeb, :html
The definitions below will be executed for every controller, The definitions below will be executed for every controller,
component, etc, so keep them short and clean, focused component, etc, so keep them short and clean, focused
@ -41,7 +41,7 @@ defmodule SimpleshopThemeWeb do
quote do quote do
use Phoenix.Controller, formats: [:html, :json] use Phoenix.Controller, formats: [:html, :json]
use Gettext, backend: SimpleshopThemeWeb.Gettext use Gettext, backend: BerrypodWeb.Gettext
import Plug.Conn import Plug.Conn
@ -81,18 +81,18 @@ defmodule SimpleshopThemeWeb do
defp html_helpers do defp html_helpers do
quote do quote do
# Translation # Translation
use Gettext, backend: SimpleshopThemeWeb.Gettext use Gettext, backend: BerrypodWeb.Gettext
# HTML escaping functionality # HTML escaping functionality
import Phoenix.HTML import Phoenix.HTML
# Core UI components # Core UI components
import SimpleshopThemeWeb.CoreComponents import BerrypodWeb.CoreComponents
# Shop UI components # Shop UI components
use SimpleshopThemeWeb.ShopComponents use BerrypodWeb.ShopComponents
# Common modules used in templates # Common modules used in templates
alias Phoenix.LiveView.JS alias Phoenix.LiveView.JS
alias SimpleshopThemeWeb.Layouts alias BerrypodWeb.Layouts
# Routes generation with the ~p sigil # Routes generation with the ~p sigil
unquote(verified_routes()) unquote(verified_routes())
@ -102,9 +102,9 @@ defmodule SimpleshopThemeWeb do
def verified_routes do def verified_routes do
quote do quote do
use Phoenix.VerifiedRoutes, use Phoenix.VerifiedRoutes,
endpoint: SimpleshopThemeWeb.Endpoint, endpoint: BerrypodWeb.Endpoint,
router: SimpleshopThemeWeb.Router, router: BerrypodWeb.Router,
statics: SimpleshopThemeWeb.static_paths() statics: BerrypodWeb.static_paths()
end end
end end

View File

@ -1,4 +1,4 @@
defmodule SimpleshopThemeWeb.AdminLayoutHook do defmodule BerrypodWeb.AdminLayoutHook do
@moduledoc """ @moduledoc """
LiveView on_mount hook that assigns the current path for admin sidebar navigation. LiveView on_mount hook that assigns the current path for admin sidebar navigation.
""" """

View File

@ -1,4 +1,4 @@
defmodule SimpleshopThemeWeb.CartHook do defmodule BerrypodWeb.CartHook do
@moduledoc """ @moduledoc """
LiveView on_mount hook for cart state and shared event handling. LiveView on_mount hook for cart state and shared event handling.
@ -19,8 +19,8 @@ defmodule SimpleshopThemeWeb.CartHook do
import Phoenix.Component, only: [assign: 3] import Phoenix.Component, only: [assign: 3]
import Phoenix.LiveView, only: [attach_hook: 4, connected?: 1, push_event: 3] import Phoenix.LiveView, only: [attach_hook: 4, connected?: 1, push_event: 3]
alias SimpleshopTheme.Cart alias Berrypod.Cart
alias SimpleshopTheme.Shipping alias Berrypod.Shipping
def on_mount(:mount_cart, _params, session, socket) do def on_mount(:mount_cart, _params, session, socket) do
cart_items = Cart.get_from_session(session) cart_items = Cart.get_from_session(session)
@ -41,7 +41,7 @@ defmodule SimpleshopThemeWeb.CartHook do
if connected?(socket) do if connected?(socket) do
csrf_token = Map.get(session, "_csrf_token", "default") csrf_token = Map.get(session, "_csrf_token", "default")
topic = "cart:#{csrf_token}" topic = "cart:#{csrf_token}"
Phoenix.PubSub.subscribe(SimpleshopTheme.PubSub, topic) Phoenix.PubSub.subscribe(Berrypod.PubSub, topic)
assign(socket, :cart_topic, topic) assign(socket, :cart_topic, topic)
else else
assign(socket, :cart_topic, nil) assign(socket, :cart_topic, nil)
@ -152,7 +152,7 @@ defmodule SimpleshopThemeWeb.CartHook do
def broadcast_and_update(socket, cart) do def broadcast_and_update(socket, cart) do
if socket.assigns.cart_topic do if socket.assigns.cart_topic do
Phoenix.PubSub.broadcast_from( Phoenix.PubSub.broadcast_from(
SimpleshopTheme.PubSub, Berrypod.PubSub,
self(), self(),
socket.assigns.cart_topic, socket.assigns.cart_topic,
{:cart_updated, cart} {:cart_updated, cart}

View File

@ -1,4 +1,4 @@
defmodule SimpleshopThemeWeb.CoreComponents do defmodule BerrypodWeb.CoreComponents do
@moduledoc """ @moduledoc """
Provides core UI components. Provides core UI components.
@ -18,7 +18,7 @@ defmodule SimpleshopThemeWeb.CoreComponents do
""" """
use Phoenix.Component use Phoenix.Component
use Gettext, backend: SimpleshopThemeWeb.Gettext use Gettext, backend: BerrypodWeb.Gettext
alias Phoenix.LiveView.JS alias Phoenix.LiveView.JS
@ -448,9 +448,9 @@ defmodule SimpleshopThemeWeb.CoreComponents do
# with our gettext backend as first argument. Translations are # with our gettext backend as first argument. Translations are
# available in the errors.po file (as we use the "errors" domain). # available in the errors.po file (as we use the "errors" domain).
if count = opts[:count] do if count = opts[:count] do
Gettext.dngettext(SimpleshopThemeWeb.Gettext, "errors", msg, msg, count, opts) Gettext.dngettext(BerrypodWeb.Gettext, "errors", msg, msg, count, opts)
else else
Gettext.dgettext(SimpleshopThemeWeb.Gettext, "errors", msg, opts) Gettext.dgettext(BerrypodWeb.Gettext, "errors", msg, opts)
end end
end end

View File

@ -1,9 +1,9 @@
defmodule SimpleshopThemeWeb.Layouts do defmodule BerrypodWeb.Layouts do
@moduledoc """ @moduledoc """
This module holds layouts and related functionality This module holds layouts and related functionality
used by your application. used by your application.
""" """
use SimpleshopThemeWeb, :html use BerrypodWeb, :html
# Embed all files in layouts/* within this module. # Embed all files in layouts/* within this module.
# The default root.html.heex file contains the HTML # The default root.html.heex file contains the HTML

View File

@ -12,7 +12,7 @@
> >
<.icon name="hero-bars-3" class="size-5" /> <.icon name="hero-bars-3" class="size-5" />
</label> </label>
<span class="admin-topbar-title">SimpleShop</span> <span class="admin-topbar-title">Berrypod</span>
<.link href={~p"/"} class="admin-btn admin-btn-ghost admin-btn-sm"> <.link href={~p"/"} class="admin-btn admin-btn-ghost admin-btn-sm">
<.icon name="hero-arrow-top-right-on-square-mini" class="size-4" /> Shop <.icon name="hero-arrow-top-right-on-square-mini" class="size-4" /> Shop
</.link> </.link>
@ -33,7 +33,7 @@
<%!-- sidebar header --%> <%!-- sidebar header --%>
<div class="p-4 border-b border-base-300"> <div class="p-4 border-b border-base-300">
<.link navigate={~p"/admin"} class="text-lg font-bold tracking-tight"> <.link navigate={~p"/admin"} class="text-lg font-bold tracking-tight">
SimpleShop Berrypod
</.link> </.link>
<p class="text-xs text-base-content/60 mt-0.5 truncate"> <p class="text-xs text-base-content/60 mt-0.5 truncate">
{@current_scope.user.email} {@current_scope.user.email}

View File

@ -4,7 +4,7 @@
<meta charset="utf-8" /> <meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" /> <meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="csrf-token" content={get_csrf_token()} /> <meta name="csrf-token" content={get_csrf_token()} />
<.live_title default="Admin" suffix=" · SimpleShop"> <.live_title default="Admin" suffix=" · Berrypod">
{assigns[:page_title]} {assigns[:page_title]}
</.live_title> </.live_title>
<!-- Pre-declare layer order so shop reset < Tailwind base regardless of load order --> <!-- Pre-declare layer order so shop reset < Tailwind base regardless of load order -->

View File

@ -4,7 +4,7 @@
<meta charset="utf-8" /> <meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" /> <meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="csrf-token" content={get_csrf_token()} /> <meta name="csrf-token" content={get_csrf_token()} />
<.live_title default="SimpleshopTheme" suffix=" · Phoenix Framework"> <.live_title default="Berrypod" suffix=" · Phoenix Framework">
{assigns[:page_title]} {assigns[:page_title]}
</.live_title> </.live_title>
<link phx-track-static rel="stylesheet" href={~p"/assets/css/admin.css"} /> <link phx-track-static rel="stylesheet" href={~p"/assets/css/admin.css"} />

View File

@ -13,9 +13,9 @@
/> />
<.live_title>{assigns[:page_title] || @theme_settings.site_name}</.live_title> <.live_title>{assigns[:page_title] || @theme_settings.site_name}</.live_title>
<!-- Preload critical fonts for the current typography preset --> <!-- Preload critical fonts for the current typography preset -->
<%= for preload <- SimpleshopTheme.Theme.Fonts.preload_links( <%= for preload <- Berrypod.Theme.Fonts.preload_links(
@theme_settings.typography, @theme_settings.typography,
&SimpleshopThemeWeb.Endpoint.static_path/1 &BerrypodWeb.Endpoint.static_path/1
) do %> ) do %>
<link rel="preload" href={preload.href} as="font" type="font/woff2" crossorigin /> <link rel="preload" href={preload.href} as="font" type="font/woff2" crossorigin />
<% end %> <% end %>

View File

@ -1,4 +1,4 @@
defmodule SimpleshopThemeWeb.PageTemplates do defmodule BerrypodWeb.PageTemplates do
@moduledoc """ @moduledoc """
Shared page templates used by both the public shop and theme preview. Shared page templates used by both the public shop and theme preview.
@ -15,7 +15,7 @@ defmodule SimpleshopThemeWeb.PageTemplates do
- `cart_count` - Number of items in cart - `cart_count` - Number of items in cart
""" """
use Phoenix.Component use Phoenix.Component
use SimpleshopThemeWeb.ShopComponents use BerrypodWeb.ShopComponents
embed_templates "page_templates/*" embed_templates "page_templates/*"
end end

View File

@ -52,7 +52,7 @@
</p> </p>
</div> </div>
<span class="checkout-item-price"> <span class="checkout-item-price">
{SimpleshopTheme.Cart.format_price(item.unit_price * item.quantity)} {Berrypod.Cart.format_price(item.unit_price * item.quantity)}
</span> </span>
</li> </li>
<% end %> <% end %>
@ -62,7 +62,7 @@
<div class="checkout-total"> <div class="checkout-total">
<span class="checkout-total-label">Total</span> <span class="checkout-total-label">Total</span>
<span class="checkout-total-amount"> <span class="checkout-total-amount">
{SimpleshopTheme.Cart.format_price(@order.total)} {Berrypod.Cart.format_price(@order.total)}
</span> </span>
</div> </div>
</div> </div>

Some files were not shown because too many files have changed in this diff Show More