From 8907fa6adc7674b7256208721f1b4cc51b15c0e7 Mon Sep 17 00:00:00 2001 From: mithereal Date: Mon, 16 Mar 2026 15:13:37 -0700 Subject: [PATCH 1/2] feat: add release seeder add release seeder, add button in settings to seed demo data --- .envrc | 1 + build.sh | 11 + config/runtime.exs | 13 +- lib/{craftday.ex => craftplan.ex} | 0 lib/craftplan/demo_seeder.ex | 401 +++++ lib/craftplan/orders/order_item.ex | 2 +- lib/craftplan/release.ex | 4 +- lib/craftplan/seed.ex | 8 + lib/craftplan/seeder.ex | 51 + lib/{craftday_web.ex => craftplan_web.ex} | 0 lib/craftplan_web/components/layouts.ex | 36 +- .../controllers/page_html/home.html.heex | 2 +- .../live/manage/customer_live/index.ex | 5 + .../live/manage/order_live/index.ex | 3 + .../manage/settings_live/form_component.ex | 39 + .../live/manage/settings_live/index.ex | 34 + mix.exs | 24 +- mix.lock | 15 +- priv/repo/seeds.exs | 1483 +---------------- .../seeds/20260317100140_add_accounts.exs | 28 + .../seeds/20260317100943_add_materials.exs | 42 + .../seeds/20260317232918_add_allergens.exs | 31 + .../20260317233646_add_nutritional_facts.exs | 31 + .../20260317234014_link_material_allergen.exs | 23 + ...7234034_link_material_nutritional_fact.exs | 27 + .../20260317234657_add_initial_stock.exs | 21 + priv/repo/seeds/20260317235225_seed_lot.exs | 40 + .../seeds/20260317235451_seed_product.exs | 32 + priv/repo/seeds/20260317235630_seed_bom.exs | 96 ++ .../seeds/20260317235851_seed_supplier.exs | 19 + .../20260318000202_seed_purchase_order.exs | 22 + ...0260318000530_seed_purchase_order_item.exs | 27 + .../seeds/20260318000703_seed_customer.exs | 38 + priv/repo/seeds/20260318000818_seed_order.exs | 43 + .../seeds/20260318000911_seed_order_items.exs | 28 + priv/static/images/{bread.svg => logo.svg} | 0 rel/overlays/bin/migrate | 4 +- rel/overlays/bin/server | 5 +- 38 files changed, 1170 insertions(+), 1519 deletions(-) create mode 100755 build.sh rename lib/{craftday.ex => craftplan.ex} (100%) create mode 100644 lib/craftplan/demo_seeder.ex create mode 100644 lib/craftplan/seed.ex create mode 100644 lib/craftplan/seeder.ex rename lib/{craftday_web.ex => craftplan_web.ex} (100%) create mode 100644 priv/repo/seeds/20260317100140_add_accounts.exs create mode 100644 priv/repo/seeds/20260317100943_add_materials.exs create mode 100644 priv/repo/seeds/20260317232918_add_allergens.exs create mode 100644 priv/repo/seeds/20260317233646_add_nutritional_facts.exs create mode 100644 priv/repo/seeds/20260317234014_link_material_allergen.exs create mode 100644 priv/repo/seeds/20260317234034_link_material_nutritional_fact.exs create mode 100644 priv/repo/seeds/20260317234657_add_initial_stock.exs create mode 100644 priv/repo/seeds/20260317235225_seed_lot.exs create mode 100644 priv/repo/seeds/20260317235451_seed_product.exs create mode 100644 priv/repo/seeds/20260317235630_seed_bom.exs create mode 100644 priv/repo/seeds/20260317235851_seed_supplier.exs create mode 100644 priv/repo/seeds/20260318000202_seed_purchase_order.exs create mode 100644 priv/repo/seeds/20260318000530_seed_purchase_order_item.exs create mode 100644 priv/repo/seeds/20260318000703_seed_customer.exs create mode 100644 priv/repo/seeds/20260318000818_seed_order.exs create mode 100644 priv/repo/seeds/20260318000911_seed_order_items.exs rename priv/static/images/{bread.svg => logo.svg} (100%) diff --git a/.envrc b/.envrc index 72ef5b2c..f77ccb15 100644 --- a/.envrc +++ b/.envrc @@ -1,2 +1,3 @@ export MIX_OS_DEPS_COMPILE_PARTITION_COUNT=10 +export SEED_DATA=true diff --git a/build.sh b/build.sh new file mode 100755 index 00000000..06993851 --- /dev/null +++ b/build.sh @@ -0,0 +1,11 @@ +#!/usr/bin/env bash +set -o errexit + +mix deps.get --only prod +MIX_ENV=prod mix compile + +MIX_ENV=prod mix assets.build +MIX_ENV=prod mix assets.deploy + +MIX_ENV=prod mix phx.gen.release +MIX_ENV=prod mix release --overwrite \ No newline at end of file diff --git a/config/runtime.exs b/config/runtime.exs index ae86db2d..88f53dd8 100644 --- a/config/runtime.exs +++ b/config/runtime.exs @@ -18,7 +18,8 @@ import Config # script that automatically sets the env var above. alias Swoosh.ApiClient.Finch -if System.get_env("PHX_SERVER") do +if System.get_env("PHX_SERVER") || + System.get_env("RAILWAY_PUBLIC_DOMAIN") do config :craftplan, CraftplanWeb.Endpoint, server: true end @@ -44,7 +45,11 @@ if config_env() == :prod do You can generate one by running: openssl rand -base64 48 """ - host = System.get_env("HOST") || System.get_env("PHX_HOST") || "example.com" + host = + System.get_env("HOST") || System.get_env("PHX_HOST") || + System.get_env("RAILWAY_PUBLIC_DOMAIN") || + "localhost" + port = String.to_integer(System.get_env("PORT") || "4000") config :craftplan, Craftplan.Repo, @@ -54,7 +59,9 @@ if config_env() == :prod do socket_options: maybe_ipv6, # Handle traffic bursts - allow queries to queue longer before failing queue_target: 500, - queue_interval: 1000 + queue_interval: 1000, + database: "craftplan", + start_apps_before_migration: [:logger] config :craftplan, CraftplanWeb.Endpoint, url: [host: host, port: 443, scheme: "https"], diff --git a/lib/craftday.ex b/lib/craftplan.ex similarity index 100% rename from lib/craftday.ex rename to lib/craftplan.ex diff --git a/lib/craftplan/demo_seeder.ex b/lib/craftplan/demo_seeder.ex new file mode 100644 index 00000000..9d1d3f82 --- /dev/null +++ b/lib/craftplan/demo_seeder.ex @@ -0,0 +1,401 @@ +defmodule Craftplan.Seeder.Demo do + @moduledoc false + alias Craftplan.Inventory.Movement + + def run do + params = [ + ["All Purpose Flour", "FLOUR-001", :gram, "0.002", "5000", "20000"], + ["Whole Wheat Flour", "FLOUR-002", :gram, "0.003", "3000", "15000"], + ["Rye Flour", "FLOUR-003", :gram, "0.004", "2000", "8000"], + ["Gluten-Free Flour Mix", "GF-001", :gram, "0.005", "1000", "7000"], + ["Gluten-Free Flour Mix", "GF-001", :gram, "0.005", "1000", "7000"], + ["Whole Almonds", "NUTS-001", :gram, "0.02", "2000", "10000"], + ["Walnuts", "NUTS-002", :gram, "0.025", "1500", "8000"], + ["Fresh Eggs", "EGG-001", :piece, "0.15", "100", "500"], + ["Whole Milk", "MILK-001", :milliliter, "0.003", "2000", "10000"], + ["Butter", "DAIRY-001", :gram, "0.01", "1000", "5000"], + ["Cream Cheese", "DAIRY-002", :gram, "0.015", "500", "3000"], + ["White Sugar", "SUGAR-001", :gram, "0.003", "3000", "15000"], + ["Brown Sugar", "SUGAR-002", :gram, "0.004", "2000", "10000"], + ["Dark Chocolate", "CHOC-001", :gram, "0.02", "2000", "8000"], + ["Vanilla Extract", "FLAV-001", :milliliter, "0.15", "500", "2000"], + ["Ground Cinnamon", "SPICE-001", :gram, "0.006", "300", "1500"], + ["Active Dry Yeast", "YEAST-001", :gram, "0.05", "500", "2000"], + ["Sea Salt", "SALT-001", :gram, "0.001", "1000", "5000"] + ] + + Enum.each(params, fn [name, sku, unit, price, min, max] -> + Craftplan.Inventory.Material + |> Ash.Changeset.for_create(:create, %{ + name: name, + sku: sku, + unit: unit, + price: Decimal.new(price), + minimum_stock: Decimal.new(min), + maximum_stock: Decimal.new(max) + }) + |> Ash.create(authorize?: false) + end) + + params = [ + "Gluten", + "Fish", + "Milk", + "Mustard", + "Lupin", + "Crustaceans", + "Peanuts", + "Tree Nuts", + "Sesame", + "Mollusks", + "Eggs", + "Soy", + "Celery", + "Sulphur Dioxide" + ] + + Enum.each(params, fn name -> + Craftplan.Inventory.Allergen + |> Ash.Changeset.for_create(:create, %{name: name}) + |> Ash.create(authorize?: false) + end) + + params = [ + "Calories", + "Fat", + "Saturated Fat", + "Carbohydrates", + "Sugar", + "Fiber", + "Protein", + "Salt", + "Sodium", + "Calcium", + "Iron", + "Vitamin A", + "Vitamin C", + "Vitamin D" + ] + + Enum.each(params, fn name -> + Craftplan.Inventory.NutritionalFact + |> Ash.Changeset.for_create(:create, %{name: name}) + |> Ash.create(authorize?: false) + end) + + materials = Craftplan.Inventory.list_materials!(authorize?: false) + allergens = Craftplan.Inventory.list_allergens!(authorize?: false) + + Enum.each(1..50, fn _ -> + material = Enum.random(materials) + allergen = Enum.random(allergens) + + Craftplan.Inventory.MaterialAllergen + |> Ash.Changeset.for_create(:create, %{ + material_id: material.id, + allergen_id: allergen.id + }) + |> Ash.create(authorize?: false) + end) + + materials = Craftplan.Inventory.list_materials!(authorize?: false) + nutritional_facts = Craftplan.Inventory.list_nutritional_facts!(authorize?: false) + + Enum.each(1..50, fn _ -> + amount = Enum.random(1..200) + material = Enum.random(materials) + nutritional_fact = Enum.random(nutritional_facts) + unit = Enum.random([:gram, :milligram, :kcal]) + + Craftplan.Inventory.MaterialNutritionalFact + |> Ash.Changeset.for_create(:create, %{ + material_id: material, + nutritional_fact_id: nutritional_fact, + amount: Decimal.new(amount), + unit: unit + }) + |> Ash.create(authorize?: false) + end) + + params = [{"Fresh Dairy Ltd.", "sales@dairy.test"}, {"Miller & Co.", "hello@miller.test"}] + + Enum.each(params, fn {name, email} -> + Craftplan.Inventory.Supplier + |> Ash.Changeset.for_create(:create, %{ + name: name, + contact_email: email + }) + |> Ash.create(authorize?: false) + end) + + suppliers = Craftplan.Inventory.list_suppliers!(authorize?: false) + + Enum.each(Craftplan.Inventory.list_materials!(authorize?: false), fn material -> + quantity = Enum.random(1..200) + + Movement + |> Ash.Changeset.for_create(:adjust_stock, %{ + material_id: material.id, + occurred_at: DateTime.utc_now(), + quantity: Decimal.new(quantity), + reason: "Initial stock" + }) + |> Ash.create(authorize?: false) + end) + + materials = Craftplan.Inventory.list_materials!(authorize?: false) + + Enum.each(1..50, fn x -> + quantity = Enum.random(1..200) + material = Enum.random(materials) + supplier = Enum.random(suppliers) + expiry_in_days = Enum.random(1..14) + lot_code = "LOT#{material.name}_#{DateTime.to_string(DateTime.utc_now())}_#{x}" + + lot = + Craftplan.Inventory.Lot + |> Ash.Changeset.for_create(:create, %{ + lot_code: lot_code, + material_id: material.id, + supplier_id: supplier && supplier.id, + received_at: DateTime.utc_now(), + expiry_date: Date.add(Date.utc_today(), expiry_in_days) + }) + |> Ash.create!(authorize?: false) + + Movement + |> Ash.Changeset.for_create(:adjust_stock, %{ + material_id: material.id, + lot_id: lot.id, + occurred_at: DateTime.utc_now(), + quantity: Decimal.new(quantity), + reason: "Received lot #{lot_code}" + }) + |> Ash.create(authorize?: false) + end) + + params = [ + ["Almond Cookies", "COOK-001", "3.99"], + ["Chocolate Cake", "CAKE-001", "15.99"], + ["Artisan Bread", "BREAD-001", "4.99"], + ["Blueberry Muffins", "MUF-001", "2.99"], + ["Butter Croissants", "PAST-001", "2.50"], + ["Gluten-Free Cupcakes", "CUP-001", "3.49"], + ["Rye Loaf Bread", "BREAD-002", "5.49"], + ["Carrot Cake", "CAKE-002", "12.99"], + ["Oatmeal Cookies", "COOK-002", "3.49"], + ["Cheese Danish", "PAST-002", "2.99"] + ] + + Enum.each(params, fn [name, sku, price] -> + Craftplan.Catalog.Product + |> Ash.Changeset.for_create(:create, %{ + name: name, + sku: sku, + status: :active, + price: Decimal.new(price) + }) + |> Ash.create(authorize?: false) + end) + + products = Craftplan.Catalog.list_products!(authorize?: false) + materials = Craftplan.Inventory.list_materials!(authorize?: false) + + labor_types = [ + "Mix & knead", + "Bake loaves", + "Bulk proof", + "Bake", + "Fill tins", + "Laminate butter", + "Proof", + "Pipe", + "Frost & decorate", + "Bake Layers", + "Cream butter & sugar", + "Fold dry ingredients", + "Prepare filling", + "Frost & finish", + "Bake tests", + "Prep dough" + ] + + labor_defs = + Enum.map(labor_types, fn x -> + %{ + name: x, + duration_minutes: Decimal.new(Enum.random(1..25)), + units_per_run: Decimal.new(Enum.random(1..25)) + } + end) + + component_defs = + Enum.map(materials, fn x -> + %{ + component_type: :material, + material_id: x.id, + quantity: Decimal.new(Enum.random(1..25)) + } + end) + + Enum.each(products, fn product -> + opts = [status: :active, name: "#{product.name}_v1"] + + component_defs = Enum.take(component_defs, Enum.random(1..Enum.count(component_defs))) + labor_defs = Enum.take(labor_defs, Enum.random(1..Enum.count(labor_defs))) + + status = Keyword.get(opts, :status, :draft) + + published_at = + case Keyword.get(opts, :published_at) do + nil -> + if status == :active do + DateTime.utc_now() + end + + value -> + value + end + + components = + component_defs + |> Enum.with_index(1) + |> Enum.map(fn {attrs, position} -> + Map.put(attrs, :position, position) + end) + + labor_steps = + labor_defs + |> Enum.with_index(1) + |> Enum.map(fn {attrs, sequence} -> + attrs + |> Map.put(:sequence, sequence) + |> Map.put_new(:units_per_run, Decimal.new("1")) + end) + + Craftplan.Catalog.BOM + |> Ash.Changeset.for_create(:create, %{ + product_id: product.id, + name: Keyword.get(opts, :name, "#{product.name} BOM"), + status: status, + published_at: published_at, + components: components, + labor_steps: labor_steps + }) + |> Ash.create!(authorize?: false) + end) + + Enum.each(1..25, fn _ -> + status = Enum.random([:draft, :ordered, :received, :cancelled]) + supplier = Enum.random(suppliers) + + Craftplan.Inventory.PurchaseOrder + |> Ash.Changeset.for_create(:create, %{ + supplier_id: supplier.id, + ordered_at: DateTime.utc_now() + }) + |> Ash.create(authorize?: false) + end) + + materials = Craftplan.Inventory.list_materials!() + purchase_orders = Craftplan.Inventory.list_purchase_orders!() + + Enum.each(purchase_orders, fn po -> + material = Enum.random(materials) + po = Enum.random(po) + quantity = Enum.random(1..200) + unit_price = Enum.random(1..200) + + Craftplan.Inventory.PurchaseOrderItem + |> Ash.Changeset.for_create(:create, %{ + purchase_order_id: po.id, + material_id: material.id, + quantity: Decimal.new(quantity), + unit_price: Decimal.new(unit_price) + }) + |> Ash.create(authorize?: false) + end) + + profiles = + for _ <- 1..50 do + email = + Faker.Person.first_name() <> + "_" <> Faker.Person.last_name() <> "@" <> Faker.Internet.En.free_email_service() + + {Faker.Person.first_name(), Faker.Person.last_name(), email, Faker.Phone.EnUs.phone(), + %{ + street: Faker.Address.En.street_address(), + city: Faker.Address.En.city(), + state: Faker.Address.En.state_abbr(), + zip: Faker.Address.En.zip_code(), + country: Faker.Address.En.country() + }} + end + + Enum.each(profiles, fn {first_name, last_name, email, phone, address_map} -> + Craftplan.CRM.Customer + |> Ash.Changeset.for_create(:create, %{ + type: :individual, + first_name: first_name, + last_name: last_name, + email: email, + phone: phone, + billing_address: address_map, + shipping_address: address_map + }) + |> Ash.create(authorize?: false) + end) + + customers = Craftplan.CRM.list_customers!(authorize?: false) + + Enum.each(1..50, fn _ -> + delivery_in_days = Enum.random(1..25) + customer = Enum.random(customers) + + status = + Enum.random([ + :delivered, + :completed, + :cancelled + ]) + + payment_status = + Enum.random([ + :none, + :issued, + :paid + ]) + + Craftplan.Orders.Order + |> Ash.Changeset.for_create(:create, %{ + customer_id: customer.id, + delivery_date: DateTime.add(DateTime.utc_now(), delivery_in_days, :day), + status: status, + invoice_status: payment_status + }) + |> Ash.create(authorize?: false) + end) + + orders = Craftplan.Orders.list_orders!(authorize?: false) + products = Craftplan.Catalog.list_products!(authorize?: false) + + Enum.each(orders, fn order -> + status = Enum.random([:todo, :in_progress, :done]) + + for i <- 1..Enum.random(1..5) do + product = Enum.random(products) + quantity = Enum.random(1..200) + + Craftplan.Orders.OrderItem + |> Ash.Changeset.for_create(:create, %{ + order_id: order.id, + product_id: product.id, + quantity: Decimal.new(quantity), + unit_price: product.price, + status: status + }) + |> Ash.create(authorize?: false) + end + end) + end +end diff --git a/lib/craftplan/orders/order_item.ex b/lib/craftplan/orders/order_item.ex index c5ac1bf7..213515b5 100644 --- a/lib/craftplan/orders/order_item.ex +++ b/lib/craftplan/orders/order_item.ex @@ -53,7 +53,7 @@ defmodule Craftplan.Orders.OrderItem do create :create do primary? true - accept [:product_id, :quantity, :unit_price, :status] + accept [:order_id, :product_id, :quantity, :unit_price, :status] change {AssignBatchCodeAndCost, []} end diff --git a/lib/craftplan/release.ex b/lib/craftplan/release.ex index 253e91e0..7d43e93d 100644 --- a/lib/craftplan/release.ex +++ b/lib/craftplan/release.ex @@ -1,12 +1,12 @@ defmodule Craftplan.Release do @moduledoc """ - Release tasks that can be run without Mix (inside OTP releases). + Used for executing DB release tasks when run in production without Mix + installed. Usage: bin/craftplan eval "Craftplan.Release.migrate" """ - @app :craftplan def migrate do diff --git a/lib/craftplan/seed.ex b/lib/craftplan/seed.ex new file mode 100644 index 00000000..722bf06e --- /dev/null +++ b/lib/craftplan/seed.ex @@ -0,0 +1,8 @@ +defmodule Craftplan.Seed do + @moduledoc false + defmacro __using__(_opts) do + quote do + use PhilColumns.Seed + end + end +end diff --git a/lib/craftplan/seeder.ex b/lib/craftplan/seeder.ex new file mode 100644 index 00000000..df2a3bb1 --- /dev/null +++ b/lib/craftplan/seeder.ex @@ -0,0 +1,51 @@ +defmodule Craftplan.Release.Seeder do + @moduledoc false + import Ecto + import Ecto.Migrator, only: [migrations_path: 2, with_repo: 2] + + @app :craftplan + + def seed(opts \\ [], seeder \\ &PhilColumns.Seeder.run/4) do + load_app() + + repos = [Craftplan.Repo] + + # set env with current_env/0 overwriting provided arg + opts = Keyword.put(opts, :env, current_env()) + opts = Keyword.put(opts, :tags, []) + + opts = + if opts[:to] || opts[:step] || opts[:all], + do: opts, + else: Keyword.put(opts, :all, true) + + opts = + if opts[:log], + do: opts, + else: Keyword.put(opts, :log, :info) + + opts = + if opts[:quiet], + do: Keyword.put(opts, :log, false), + else: opts + + for repo <- repos() do + {:ok, _, _} = with_repo(repo, &seeder.(&1, migrations_path(&1, "seeds"), :up, opts)) + end + end + + defp current_env do + case Application.fetch_env(@app, :env) do + :error -> :dev + {:ok, data} -> data + end + end + + defp repos do + Application.fetch_env!(@app, :ecto_repos) + end + + defp load_app do + Application.load(@app) + end +end diff --git a/lib/craftday_web.ex b/lib/craftplan_web.ex similarity index 100% rename from lib/craftday_web.ex rename to lib/craftplan_web.ex diff --git a/lib/craftplan_web/components/layouts.ex b/lib/craftplan_web/components/layouts.ex index a33fc4c3..141e00f9 100644 --- a/lib/craftplan_web/components/layouts.ex +++ b/lib/craftplan_web/components/layouts.ex @@ -674,40 +674,8 @@ defmodule CraftplanWeb.Layouts do defp logo_link(assigns) do ~H""" <.link navigate={~p"/"} class="flex items-center gap-2"> - - - - - - - - - - - Craftplan - + Logo + Craftplan """ end diff --git a/lib/craftplan_web/controllers/page_html/home.html.heex b/lib/craftplan_web/controllers/page_html/home.html.heex index 637b98e1..5eeb7135 100644 --- a/lib/craftplan_web/controllers/page_html/home.html.heex +++ b/lib/craftplan_web/controllers/page_html/home.html.heex @@ -1,7 +1,7 @@
- Craftplan Logo + Logo

Craftplan · {@release_version} diff --git a/lib/craftplan_web/live/manage/customer_live/index.ex b/lib/craftplan_web/live/manage/customer_live/index.ex index 9eb470a7..0e429a13 100644 --- a/lib/craftplan_web/live/manage/customer_live/index.ex +++ b/lib/craftplan_web/live/manage/customer_live/index.ex @@ -10,6 +10,11 @@ defmodule CraftplanWeb.CustomerLive.Index do <.header> Customers <:subtitle>Manage your customer records + <:actions> + <.link patch={~p"/manage/customers/new"}> + <.button variant={:primary}>New Customer + + <.table diff --git a/lib/craftplan_web/live/manage/order_live/index.ex b/lib/craftplan_web/live/manage/order_live/index.ex index 01028847..8e047b6b 100644 --- a/lib/craftplan_web/live/manage/order_live/index.ex +++ b/lib/craftplan_web/live/manage/order_live/index.ex @@ -57,6 +57,9 @@ defmodule CraftplanWeb.OrderLive.Index do <:actions> + <.link patch={~p"/manage/orders/new"}> + <.button variant={:primary}>New Order +

diff --git a/lib/craftplan_web/live/manage/settings_live/form_component.ex b/lib/craftplan_web/live/manage/settings_live/form_component.ex index a5c4853d..93df988b 100644 --- a/lib/craftplan_web/live/manage/settings_live/form_component.ex +++ b/lib/craftplan_web/live/manage/settings_live/form_component.ex @@ -327,6 +327,45 @@ defmodule CraftplanWeb.SettingsLive.FormComponent do
+
+
+

+ Demo Data +

+
+
+

+ Seed the database with demo data. +

+ <.button variant={:secondary} phx-click="seed_demo" phx-disable-with="Seeding..."> + Seed + +
+
+ +
+
+

+ Danger +

+
+
+

+ Reset all the database data (except users/settings). +

+ <.button variant={:danger} phx-click="delete_data" phx-disable-with="resetting..."> + Reset + +
+
<:actions> <.button variant={:primary} phx-disable-with="Saving...">Save Settings diff --git a/lib/craftplan_web/live/manage/settings_live/index.ex b/lib/craftplan_web/live/manage/settings_live/index.ex index c12fd0fd..e83093ad 100644 --- a/lib/craftplan_web/live/manage/settings_live/index.ex +++ b/lib/craftplan_web/live/manage/settings_live/index.ex @@ -3,6 +3,7 @@ defmodule CraftplanWeb.SettingsLive.Index do use CraftplanWeb, :live_view alias Craftplan.Inventory + alias Craftplan.Repo alias Craftplan.Settings alias CraftplanWeb.Navigation @@ -206,6 +207,39 @@ defmodule CraftplanWeb.SettingsLive.Index do {:noreply, Navigation.assign(socket, :settings, settings_trail(live_action))} end + @impl true + def handle_event("seed_demo", _, socket) do + Craftplan.Seeder.Demo.run() + {:noreply, socket} + end + + @impl true + def handle_event("delete_data", _, socket) do + Repo.delete_all(Craftplan.Orders.ProductionBatchLot) + Repo.delete_all(Craftplan.Orders.OrderItemBatchAllocation) + Repo.delete_all(Craftplan.Orders.OrderItemLot) + Repo.delete_all(Craftplan.Orders.OrderItem) + Repo.delete_all(Craftplan.Orders.Order) + Repo.delete_all(Craftplan.Catalog.BOMRollup) + Repo.delete_all(Craftplan.Catalog.BOMComponent) + Repo.delete_all(Craftplan.Catalog.LaborStep) + Repo.delete_all(Craftplan.Catalog.BOM) + Repo.delete_all(Craftplan.Catalog.Product) + Repo.delete_all(Craftplan.Inventory.Movement) + Repo.delete_all(Craftplan.Inventory.Lot) + Repo.delete_all(Craftplan.Inventory.MaterialNutritionalFact) + Repo.delete_all(Craftplan.Inventory.NutritionalFact) + Repo.delete_all(Craftplan.Inventory.MaterialAllergen) + Repo.delete_all(Craftplan.Inventory.PurchaseOrderItem) + Repo.delete_all(Craftplan.Inventory.PurchaseOrder) + Repo.delete_all(Craftplan.Inventory.Supplier) + Repo.delete_all(Craftplan.Orders.ProductionBatch) + Repo.delete_all(Craftplan.Inventory.Material) + Repo.delete_all(Craftplan.Inventory.Allergen) + Repo.delete_all(Craftplan.CRM.Customer) + {:noreply, socket} + end + @impl true def handle_event("open_import", %{"entity" => entity}, socket) do {:noreply, diff --git a/mix.exs b/mix.exs index f441c353..49f00c28 100644 --- a/mix.exs +++ b/mix.exs @@ -11,7 +11,15 @@ defmodule Craftplan.MixProject do consolidate_protocols: Mix.env() != :dev, listeners: [Phoenix.CodeReloader], aliases: aliases(), - deps: deps() + deps: deps(), + test_coverage: [tool: ExCoveralls], + preferred_cli_env: [ + coveralls: :test, + "coveralls.detail": :test, + "coveralls.post": :test, + "coveralls.html": :test, + "coveralls.cobertura": :test + ] ] end @@ -21,7 +29,13 @@ defmodule Craftplan.MixProject do def application do [ mod: {Craftplan.Application, []}, - extra_applications: [:logger, :runtime_tools] + extra_applications: [:logger, :runtime_tools, :glific_phil_columns] + ] + end + + def cli do + [ + preferred_envs: [precommit: :test] ] end @@ -89,7 +103,10 @@ defmodule Craftplan.MixProject do {:icalendar, "~> 1.1"}, {:imprintor, "~> 0.5"}, {:open_api_spex, "~> 3.16"}, - {:req, "~> 0.5", only: [:dev, :test]} + {:req, "~> 0.5", only: [:dev, :test]}, + {:glific_phil_columns, github: "data-twister/phil_columns-ex"}, + {:excoveralls, "~> 0.18.5"}, + {:faker, "~> 0.18.0"} ] end @@ -101,6 +118,7 @@ defmodule Craftplan.MixProject do # See the documentation for `Mix` for more info on aliases. defp aliases do [ + seed: ["phil_columns.seed"], setup: ["deps.get", "ash.setup", "assets.setup", "assets.build", "run priv/repo/seeds.exs"], "ecto.setup": ["ecto.create", "ecto.migrate", "run priv/repo/seeds.exs"], "ecto.reset": ["ecto.drop", "ecto.setup"], diff --git a/mix.lock b/mix.lock index 1c35290b..5923ace4 100644 --- a/mix.lock +++ b/mix.lock @@ -1,6 +1,7 @@ %{ "absinthe": {:hex, :absinthe, "1.9.0", "28f11753d01c0e8b6cb6e764a23cf4081e0e6cae88f53f4c9e4320912aee9c07", [:mix], [{:dataloader, "~> 1.0.0 or ~> 2.0", [hex: :dataloader, repo: "hexpm", optional: true]}, {:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}, {:nimble_parsec, "~> 1.2.2 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}, {:opentelemetry_process_propagator, "~> 0.2.1 or ~> 0.3", [hex: :opentelemetry_process_propagator, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "db65993420944ad90e932827663d4ab704262b007d4e3900cd69615f14ccc8ce"}, "absinthe_plug": {:hex, :absinthe_plug, "1.5.9", "4f66fd46aecf969b349dd94853e6132db6d832ae6a4b951312b6926ad4ee7ca3", [:mix], [{:absinthe, "~> 1.7", [hex: :absinthe, repo: "hexpm", optional: false]}, {:plug, "~> 1.4", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "dcdc84334b0e9e2cd439bd2653678a822623f212c71088edf0a4a7d03f1fa225"}, + "artificery": {:hex, :artificery, "0.4.4", "04ef46408596052053f0152b08d86b31a85b63862613f7fc13d6fb4f64819f6a", [:mix], [], "hexpm", "b897b4986dc1bb3e2f5c86de5bb4ea101b656441d16fc0db73622c09c18e96b7"}, "ash": {:hex, :ash, "3.12.0", "5b78000df650d86b446d88977ef8aa5c9d9f7ffa1193fa3c4b901c60bff2d130", [:mix], [{:crux, ">= 0.1.2 and < 1.0.0-0", [hex: :crux, repo: "hexpm", optional: false]}, {:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:ecto, "~> 3.7", [hex: :ecto, repo: "hexpm", optional: false]}, {:ets, "~> 0.8", [hex: :ets, repo: "hexpm", optional: false]}, {:igniter, ">= 0.6.29 and < 1.0.0-0", [hex: :igniter, repo: "hexpm", optional: true]}, {:jason, ">= 1.0.0", [hex: :jason, repo: "hexpm", optional: false]}, {:picosat_elixir, "~> 0.2", [hex: :picosat_elixir, repo: "hexpm", optional: true]}, {:plug, ">= 0.0.0", [hex: :plug, repo: "hexpm", optional: true]}, {:reactor, "~> 0.11", [hex: :reactor, repo: "hexpm", optional: false]}, {:simple_sat, ">= 0.1.1 and < 1.0.0-0", [hex: :simple_sat, repo: "hexpm", optional: true]}, {:spark, ">= 2.3.14 and < 3.0.0-0", [hex: :spark, repo: "hexpm", optional: false]}, {:splode, ">= 0.2.6 and < 1.0.0-0", [hex: :splode, repo: "hexpm", optional: false]}, {:stream_data, "~> 1.0", [hex: :stream_data, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.1", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "7cf45b4eb83aa0ab5e6707d6e4ea4a10c29ab20613c87f06344f7953b2ca5e18"}, "ash_admin": {:hex, :ash_admin, "0.13.24", "4fafddc7b4450a92878b58630688c55cab20b0c27e35cad68f29811f73815816", [:mix], [{:ash, ">= 3.4.63 and < 4.0.0-0", [hex: :ash, repo: "hexpm", optional: false]}, {:ash_phoenix, ">= 2.1.8 and < 3.0.0-0", [hex: :ash_phoenix, repo: "hexpm", optional: false]}, {:gettext, "~> 0.26 or ~> 1.0", [hex: :gettext, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.7", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 4.1", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:phoenix_live_view, "~> 1.1-rc", [hex: :phoenix_live_view, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: false]}], "hexpm", "8f298cf6cb6635744ed75dd401295ed52513ea4df169f0f89d6a9a75dc4de4dc"}, "ash_authentication": {:hex, :ash_authentication, "4.13.7", "421b5ddb516026f6794435980a632109ec116af2afa68a45e15fb48b41c92cfa", [:mix], [{:argon2_elixir, "~> 4.0", [hex: :argon2_elixir, repo: "hexpm", optional: true]}, {:ash, "~> 3.7", [hex: :ash, repo: "hexpm", optional: false]}, {:ash_postgres, ">= 2.6.8 and < 3.0.0-0", [hex: :ash_postgres, repo: "hexpm", optional: true]}, {:assent, "> 0.2.0 and < 0.3.0", [hex: :assent, repo: "hexpm", optional: false]}, {:bcrypt_elixir, "~> 3.0", [hex: :bcrypt_elixir, repo: "hexpm", optional: false]}, {:castore, "~> 1.0", [hex: :castore, repo: "hexpm", optional: false]}, {:finch, "~> 0.19", [hex: :finch, repo: "hexpm", optional: false]}, {:igniter, "~> 0.4", [hex: :igniter, repo: "hexpm", optional: true]}, {:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}, {:joken, "~> 2.5", [hex: :joken, repo: "hexpm", optional: false]}, {:plug, "~> 1.13", [hex: :plug, repo: "hexpm", optional: false]}, {:spark, "~> 2.0", [hex: :spark, repo: "hexpm", optional: false]}, {:splode, "~> 0.2", [hex: :splode, repo: "hexpm", optional: false]}], "hexpm", "0d45ac3fdcca6902dabbe161ce63e9cea8f90583863c2e14261c9309e5837121"}, @@ -25,14 +26,15 @@ "conv_case": {:hex, :conv_case, "0.2.3", "c1455c27d3c1ffcdd5f17f1e91f40b8a0bc0a337805a6e8302f441af17118ed8", [:mix], [], "hexpm", "88f29a3d97d1742f9865f7e394ed3da011abb7c5e8cc104e676fdef6270d4b4a"}, "cors_plug": {:hex, :cors_plug, "3.0.3", "7c3ac52b39624bc616db2e937c282f3f623f25f8d550068b6710e58d04a0e330", [:mix], [{:plug, "~> 1.13", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "3f2d759e8c272ed3835fab2ef11b46bddab8c1ab9528167bd463b6452edf830d"}, "crux": {:hex, :crux, "0.1.2", "4441c9e3a34f1e340954ce96b9ad5a2de13ceb4f97b3f910211227bb92e2ca90", [:mix], [{:picosat_elixir, "~> 0.2", [hex: :picosat_elixir, repo: "hexpm", optional: true]}, {:simple_sat, ">= 0.1.1 and < 1.0.0-0", [hex: :simple_sat, repo: "hexpm", optional: true]}, {:stream_data, "~> 1.0", [hex: :stream_data, repo: "hexpm", optional: true]}], "hexpm", "563ea3748ebfba9cc078e6d198a1d6a06015a8fae503f0b721363139f0ddb350"}, - "db_connection": {:hex, :db_connection, "2.8.1", "9abdc1e68c34c6163f6fb96a96532272d13ad7ca45262156ae8b7ec6d9dc4bec", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "a61a3d489b239d76f326e03b98794fb8e45168396c925ef25feb405ed09da8fd"}, + "db_connection": {:hex, :db_connection, "2.9.0", "a6a97c5c958a2d7091a58a9be40caf41ab496b0701d21e1d1abff3fa27a7f371", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "17d502eacaf61829db98facf6f20808ed33da6ccf495354a41e64fe42f9c509c"}, "decimal": {:hex, :decimal, "2.3.0", "3ad6255aa77b4a3c4f818171b12d237500e63525c2fd056699967a3e7ea20f62", [:mix], [], "hexpm", "a4d66355cb29cb47c3cf30e71329e58361cfcb37c34235ef3bf1d7bf3773aeac"}, "dialyxir": {:hex, :dialyxir, "1.4.6", "7cca478334bf8307e968664343cbdb432ee95b4b68a9cba95bdabb0ad5bdfd9a", [:mix], [{:erlex, ">= 0.2.7", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "8cf5615c5cd4c2da6c501faae642839c8405b49f8aa057ad4ae401cb808ef64d"}, "digital_token": {:hex, :digital_token, "1.0.0", "454a4444061943f7349a51ef74b7fb1ebd19e6a94f43ef711f7dae88c09347df", [:mix], [{:cldr_utils, "~> 2.17", [hex: :cldr_utils, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm", "8ed6f5a8c2fa7b07147b9963db506a1b4c7475d9afca6492136535b064c9e9e6"}, + "distillery": {:hex, :distillery, "2.1.1", "f9332afc2eec8a1a2b86f22429e068ef35f84a93ea1718265e740d90dd367814", [:mix], [{:artificery, "~> 0.2", [hex: :artificery, repo: "hexpm", optional: false]}], "hexpm", "bbc7008b0161a6f130d8d903b5b3232351fccc9c31a991f8fcbf2a12ace22995"}, "dns_cluster": {:hex, :dns_cluster, "0.2.0", "aa8eb46e3bd0326bd67b84790c561733b25c5ba2fe3c7e36f28e88f384ebcb33", [:mix], [], "hexpm", "ba6f1893411c69c01b9e8e8f772062535a4cf70f3f35bcc964a324078d8c8240"}, "earmark": {:hex, :earmark, "1.4.47", "7e7596b84fe4ebeb8751e14cbaeaf4d7a0237708f2ce43630cfd9065551f94ca", [:mix], [], "hexpm", "3e96bebea2c2d95f3b346a7ff22285bc68a99fbabdad9b655aa9c6be06c698f8"}, - "ecto": {:hex, :ecto, "3.13.3", "6a983f0917f8bdc7a89e96f2bf013f220503a0da5d8623224ba987515b3f0d80", [:mix], [{:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "1927db768f53a88843ff25b6ba7946599a8ca8a055f69ad8058a1432a399af94"}, - "ecto_sql": {:hex, :ecto_sql, "3.13.2", "a07d2461d84107b3d037097c822ffdd36ed69d1cf7c0f70e12a3d1decf04e2e1", [:mix], [{:db_connection, "~> 2.4.1 or ~> 2.5", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.13.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.7", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.19 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:tds, "~> 2.1.1 or ~> 2.2", [hex: :tds, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "539274ab0ecf1a0078a6a72ef3465629e4d6018a3028095dc90f60a19c371717"}, + "ecto": {:hex, :ecto, "3.13.5", "9d4a69700183f33bf97208294768e561f5c7f1ecf417e0fa1006e4a91713a834", [:mix], [{:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "df9efebf70cf94142739ba357499661ef5dbb559ef902b68ea1f3c1fabce36de"}, + "ecto_sql": {:hex, :ecto_sql, "3.13.5", "2f8282b2ad97bf0f0d3217ea0a6fff320ead9e2f8770f810141189d182dc304e", [:mix], [{:db_connection, "~> 2.4.1 or ~> 2.5", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.13.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.7", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.19 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:tds, "~> 2.1.1 or ~> 2.2", [hex: :tds, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "aa36751f4e6a2b56ae79efb0e088042e010ff4935fc8684e74c23b1f49e25fdc"}, "elixir_make": {:hex, :elixir_make, "0.9.0", "6484b3cd8c0cee58f09f05ecaf1a140a8c97670671a6a0e7ab4dc326c3109726", [:mix], [], "hexpm", "db23d4fd8b757462ad02f8aa73431a426fe6671c80b200d9710caf3d1dd0ffdb"}, "erlex": {:hex, :erlex, "0.2.7", "810e8725f96ab74d17aac676e748627a07bc87eb950d2b83acd29dc047a30595", [:mix], [], "hexpm", "3ed95f79d1a844c3f6bf0cea61e0d5612a42ce56da9c03f01df538685365efb0"}, "esbuild": {:hex, :esbuild, "0.10.0", "b0aa3388a1c23e727c5a3e7427c932d89ee791746b0081bbe56103e9ef3d291f", [:mix], [{:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "468489cda427b974a7cc9f03ace55368a83e1a7be12fba7e30969af78e5f8c70"}, @@ -45,13 +47,16 @@ "ex_image_info": {:hex, :ex_image_info, "1.0.0", "025fd15f9c02e91952207b6ffb32076caada9a53efa8c7b8556a114caa7767be", [:mix], [], "hexpm", "1b789b1331508f4024908d8e945cd9bdea463d59f19e9ad529dde0a0593894d4"}, "ex_money": {:hex, :ex_money, "5.23.0", "d3454358de76c561470e4f50ce044d45d6df2d4e2600315cf32b93036410e94e", [:mix], [{:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:ex_cldr_numbers, "~> 2.34", [hex: :ex_cldr_numbers, repo: "hexpm", optional: false]}, {:ex_cldr_units, "~> 3.19", [hex: :ex_cldr_units, repo: "hexpm", optional: true]}, {:gringotts, "~> 1.1", [hex: :gringotts, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:nimble_parsec, "~> 0.5 or ~> 1.0", [hex: :nimble_parsec, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.0 or ~> 3.0 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:poison, "~> 3.0 or ~> 4.0 or ~> 5.0 or ~> 6.0", [hex: :poison, repo: "hexpm", optional: true]}], "hexpm", "397f77b02a7939fa0e8286819f18406540be201f7f3d6c04070654995dac32d2"}, "ex_money_sql": {:hex, :ex_money_sql, "1.11.0", "1b9b2f920d5d9220fa6dd4d8aae258daf562deaed2fb037b38b1f7ba4d0a344c", [:mix], [{:ecto, "~> 3.5", [hex: :ecto, repo: "hexpm", optional: false]}, {:ecto_sql, "~> 3.0", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:ex_money, "~> 5.7", [hex: :ex_money, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:postgrex, "~> 0.15", [hex: :postgrex, repo: "hexpm", optional: false]}], "hexpm", "629e0541ae9f87122d34650f8c8febbc7349bbc6f881cf7a51b4d0779886107d"}, + "excoveralls": {:hex, :excoveralls, "0.18.5", "e229d0a65982613332ec30f07940038fe451a2e5b29bce2a5022165f0c9b157e", [:mix], [{:castore, "~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "523fe8a15603f86d64852aab2abe8ddbd78e68579c8525ae765facc5eae01562"}, "expo": {:hex, :expo, "1.1.0", "f7b9ed7fb5745ebe1eeedf3d6f29226c5dd52897ac67c0f8af62a07e661e5c75", [:mix], [], "hexpm", "fbadf93f4700fb44c331362177bdca9eeb8097e8b0ef525c9cc501cb9917c960"}, + "faker": {:hex, :faker, "0.18.0", "943e479319a22ea4e8e39e8e076b81c02827d9302f3d32726c5bf82f430e6e14", [:mix], [], "hexpm", "bfbdd83958d78e2788e99ec9317c4816e651ad05e24cfd1196ce5db5b3e81797"}, "file_system": {:hex, :file_system, "1.1.1", "31864f4685b0148f25bd3fbef2b1228457c0c89024ad67f7a81a3ffbc0bbad3a", [:mix], [], "hexpm", "7a15ff97dfe526aeefb090a7a9d3d03aa907e100e262a0f8f7746b78f8f87a5d"}, "finch": {:hex, :finch, "0.20.0", "5330aefb6b010f424dcbbc4615d914e9e3deae40095e73ab0c1bb0968933cadf", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.6.2 or ~> 1.7", [hex: :mint, repo: "hexpm", optional: false]}, {:nimble_options, "~> 0.4 or ~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:nimble_pool, "~> 1.1", [hex: :nimble_pool, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "2658131a74d051aabfcba936093c903b8e89da9a1b63e430bee62045fa9b2ee2"}, "fine": {:hex, :fine, "0.1.4", "b19a89c1476c7c57afb5f9314aed5960b5bc95d5277de4cb5ee8e1d1616ce379", [:mix], [], "hexpm", "be3324cc454a42d80951cf6023b9954e9ff27c6daa255483b3e8d608670303f5"}, "floki": {:hex, :floki, "0.38.0", "62b642386fa3f2f90713f6e231da0fa3256e41ef1089f83b6ceac7a3fd3abf33", [:mix], [], "hexpm", "a5943ee91e93fb2d635b612caf5508e36d37548e84928463ef9dd986f0d1abd9"}, "gen_smtp": {:hex, :gen_smtp, "1.3.0", "62c3d91f0dcf6ce9db71bcb6881d7ad0d1d834c7f38c13fa8e952f4104a8442e", [:rebar3], [{:ranch, ">= 1.8.0", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "0b73fbf069864ecbce02fe653b16d3f35fd889d0fdd4e14527675565c39d84e6"}, "gettext": {:hex, :gettext, "0.26.2", "5978aa7b21fada6deabf1f6341ddba50bc69c999e812211903b169799208f2a8", [:mix], [{:expo, "~> 0.5.1 or ~> 1.0", [hex: :expo, repo: "hexpm", optional: false]}], "hexpm", "aa978504bcf76511efdc22d580ba08e2279caab1066b76bb9aa81c4a1e0a32a5"}, + "glific_phil_columns": {:git, "https://github.com/data-twister/phil_columns-ex.git", "110c17f8a37747bbe228196f7a2671a9ea95f96a", []}, "glob_ex": {:hex, :glob_ex, "0.1.11", "cb50d3f1ef53f6ca04d6252c7fde09fd7a1cf63387714fe96f340a1349e62c93", [:mix], [], "hexpm", "342729363056e3145e61766b416769984c329e4378f1d558b63e341020525de4"}, "hackney": {:hex, :hackney, "1.25.0", "390e9b83f31e5b325b9f43b76e1a785cbdb69b5b6cd4e079aa67835ded046867", [:rebar3], [{:certifi, "~> 2.15.0", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "~> 6.1.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "~> 1.0.0", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~> 1.4", [hex: :mimerl, repo: "hexpm", optional: false]}, {:parse_trans, "3.4.1", [hex: :parse_trans, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "~> 1.1.0", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}, {:unicode_util_compat, "~> 0.7.1", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "7209bfd75fd1f42467211ff8f59ea74d6f2a9e81cbcee95a56711ee79fd6b1d4"}, "heroicons": {:git, "https://github.com/tailwindlabs/heroicons.git", "0435d4ca364a608cc75e2f8683d374e55abbae26", [tag: "v2.2.0", sparse: "optimized", depth: 1]}, @@ -97,7 +102,7 @@ "picosat_elixir": {:hex, :picosat_elixir, "0.2.3", "bf326d0f179fbb3b706bb2c15fbc367dacfa2517157d090fdfc32edae004c597", [:make, :mix], [{:elixir_make, "~> 0.6", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "f76c9db2dec9d2561ffaa9be35f65403d53e984e8cd99c832383b7ab78c16c66"}, "plug": {:hex, :plug, "1.18.1", "5067f26f7745b7e31bc3368bc1a2b818b9779faa959b49c934c17730efc911cf", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "57a57db70df2b422b564437d2d33cf8d33cd16339c1edb190cd11b1a3a546cc2"}, "plug_crypto": {:hex, :plug_crypto, "2.1.1", "19bda8184399cb24afa10be734f84a16ea0a2bc65054e23a62bb10f06bc89491", [:mix], [], "hexpm", "6470bce6ffe41c8bd497612ffde1a7e4af67f36a15eea5f921af71cf3e11247c"}, - "postgrex": {:hex, :postgrex, "0.21.1", "2c5cc830ec11e7a0067dd4d623c049b3ef807e9507a424985b8dcf921224cd88", [:mix], [{:db_connection, "~> 2.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.5 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "27d8d21c103c3cc68851b533ff99eef353e6a0ff98dc444ea751de43eb48bdac"}, + "postgrex": {:hex, :postgrex, "0.22.0", "fb027b58b6eab1f6de5396a2abcdaaeb168f9ed4eccbb594e6ac393b02078cbd", [:mix], [{:db_connection, "~> 2.9", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.5 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "a68c4261e299597909e03e6f8ff5a13876f5caadaddd0d23af0d0a61afcc5d84"}, "ranch": {:hex, :ranch, "2.2.0", "25528f82bc8d7c6152c57666ca99ec716510fe0925cb188172f41ce93117b1b0", [:make, :rebar3], [], "hexpm", "fa0b99a1780c80218a4197a59ea8d3bdae32fbff7e88527d7d8a4787eff4f8e7"}, "reactor": {:hex, :reactor, "0.17.0", "eb8bdb530dbae824e2d36a8538f8ec4f3aa7c2d1b61b04959fa787c634f88b49", [:mix], [{:igniter, "~> 0.4", [hex: :igniter, repo: "hexpm", optional: true]}, {:iterex, "~> 0.1", [hex: :iterex, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:libgraph, "~> 0.16", [hex: :libgraph, repo: "hexpm", optional: false]}, {:spark, ">= 2.3.3 and < 3.0.0-0", [hex: :spark, repo: "hexpm", optional: false]}, {:splode, "~> 0.2", [hex: :splode, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.2", [hex: :telemetry, repo: "hexpm", optional: false]}, {:yaml_elixir, "~> 2.11", [hex: :yaml_elixir, repo: "hexpm", optional: false]}, {:ymlr, "~> 5.0", [hex: :ymlr, repo: "hexpm", optional: false]}], "hexpm", "3c3bf71693adbad9117b11ec83cfed7d5851b916ade508ed9718de7ae165bf25"}, "req": {:hex, :req, "0.5.15", "662020efb6ea60b9f0e0fac9be88cd7558b53fe51155a2d9899de594f9906ba9", [:mix], [{:brotli, "~> 0.3.1", [hex: :brotli, repo: "hexpm", optional: true]}, {:ezstd, "~> 1.0", [hex: :ezstd, repo: "hexpm", optional: true]}, {:finch, "~> 0.17", [hex: :finch, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mime, "~> 2.0.6 or ~> 2.1", [hex: :mime, repo: "hexpm", optional: false]}, {:nimble_csv, "~> 1.0", [hex: :nimble_csv, repo: "hexpm", optional: true]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "a6513a35fad65467893ced9785457e91693352c70b58bbc045b47e5eb2ef0c53"}, @@ -115,7 +120,7 @@ "swoosh": {:hex, :swoosh, "1.19.8", "0576f2ea96d1bb3a6e02cc9f79cbd7d497babc49a353eef8dce1a1f9f82d7915", [:mix], [{:bandit, ">= 1.0.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:cowboy, "~> 1.1 or ~> 2.4", [hex: :cowboy, repo: "hexpm", optional: true]}, {:ex_aws, "~> 2.1", [hex: :ex_aws, repo: "hexpm", optional: true]}, {:finch, "~> 0.6", [hex: :finch, repo: "hexpm", optional: true]}, {:gen_smtp, "~> 0.13 or ~> 1.0", [hex: :gen_smtp, repo: "hexpm", optional: true]}, {:hackney, "~> 1.9", [hex: :hackney, repo: "hexpm", optional: true]}, {:idna, "~> 6.0", [hex: :idna, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mail, "~> 0.2", [hex: :mail, repo: "hexpm", optional: true]}, {:mime, "~> 1.1 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mua, "~> 0.2.3", [hex: :mua, repo: "hexpm", optional: true]}, {:multipart, "~> 0.4", [hex: :multipart, repo: "hexpm", optional: true]}, {:plug, "~> 1.9", [hex: :plug, repo: "hexpm", optional: true]}, {:plug_cowboy, ">= 1.0.0", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:req, "~> 0.5.10 or ~> 0.6 or ~> 1.0", [hex: :req, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "d7503c2daf0f9899afd8eba9923eeddef4b62e70816e1d3b6766e4d6c60e94ad"}, "tailwind": {:hex, :tailwind, "0.4.1", "e7bcc222fe96a1e55f948e76d13dd84a1a7653fb051d2a167135db3b4b08d3e9", [:mix], [], "hexpm", "6249d4f9819052911120dbdbe9e532e6bd64ea23476056adb7f730aa25c220d1"}, "tailwind_formatter": {:hex, :tailwind_formatter, "0.4.2", "9d1a17ab0ba8780cd662abd32b04a6fe37c22fb52522a11b326fede6f2e79edb", [:mix], [], "hexpm", "c42238f70cd9c5d90fde8162623dc19f3939eb633490f2a6ae108a1ceeb4f59c"}, - "telemetry": {:hex, :telemetry, "1.3.0", "fedebbae410d715cf8e7062c96a1ef32ec22e764197f70cda73d82778d61e7a2", [:rebar3], [], "hexpm", "7015fc8919dbe63764f4b4b87a95b7c0996bd539e0d499be6ec9d7f3875b79e6"}, + "telemetry": {:hex, :telemetry, "1.4.1", "ab6de178e2b29b58e8256b92b382ea3f590a47152ca3651ea857a6cae05ac423", [:rebar3], [], "hexpm", "2172e05a27531d3d31dd9782841065c50dd5c3c7699d95266b2edd54c2dafa1c"}, "telemetry_metrics": {:hex, :telemetry_metrics, "1.1.0", "5bd5f3b5637e0abea0426b947e3ce5dd304f8b3bc6617039e2b5a008adc02f8f", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "e7b79e8ddfde70adb6db8a6623d1778ec66401f366e9a8f5dd0955c56bc8ce67"}, "telemetry_poller": {:hex, :telemetry_poller, "1.3.0", "d5c46420126b5ac2d72bc6580fb4f537d35e851cc0f8dbd571acf6d6e10f5ec7", [:rebar3], [{:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "51f18bed7128544a50f75897db9974436ea9bfba560420b646af27a9a9b35211"}, "text_diff": {:hex, :text_diff, "0.1.0", "1caf3175e11a53a9a139bc9339bd607c47b9e376b073d4571c031913317fecaa", [:mix], [], "hexpm", "d1ffaaecab338e49357b6daa82e435f877e0649041ace7755583a0ea3362dbd7"}, diff --git a/priv/repo/seeds.exs b/priv/repo/seeds.exs index c2445172..d62f887f 100644 --- a/priv/repo/seeds.exs +++ b/priv/repo/seeds.exs @@ -8,97 +8,22 @@ alias Craftplan.Orders alias Craftplan.Repo alias Craftplan.Settings -require Ash.Query - -# ------------------------------------------------------------------------------ -# 1. Define helper functions for readability and code organization -# ------------------------------------------------------------------------------ - -seed_allergens = fn -> - seed_single_allergen = fn name -> - [allergen] = - Ash.Seed.seed!( - Inventory.Allergen, - [%{name: name}], - identity: :name - ) - - allergen - end - - %{ - gluten: seed_single_allergen.("Gluten"), - fish: seed_single_allergen.("Fish"), - milk: seed_single_allergen.("Milk"), - mustard: seed_single_allergen.("Mustard"), - lupin: seed_single_allergen.("Lupin"), - crustaceans: seed_single_allergen.("Crustaceans"), - peanuts: seed_single_allergen.("Peanuts"), - nuts: seed_single_allergen.("Tree Nuts"), - sesame: seed_single_allergen.("Sesame"), - mollusks: seed_single_allergen.("Mollusks"), - eggs: seed_single_allergen.("Eggs"), - soy: seed_single_allergen.("Soy"), - celery: seed_single_allergen.("Celery"), - sulphur: seed_single_allergen.("Sulphur Dioxide") - } -end - -# Add a function to seed nutritional facts -seed_nutritional_facts = fn -> - seed_single_nutritional_fact = fn name -> - [nutritional_fact] = - Ash.Seed.seed!( - Inventory.NutritionalFact, - [%{name: name}], - identity: :name - ) - - nutritional_fact - end - - %{ - calories: seed_single_nutritional_fact.("Calories"), - fat: seed_single_nutritional_fact.("Fat"), - saturated_fat: seed_single_nutritional_fact.("Saturated Fat"), - carbohydrates: seed_single_nutritional_fact.("Carbohydrates"), - sugar: seed_single_nutritional_fact.("Sugar"), - fiber: seed_single_nutritional_fact.("Fiber"), - protein: seed_single_nutritional_fact.("Protein"), - salt: seed_single_nutritional_fact.("Salt"), - sodium: seed_single_nutritional_fact.("Sodium"), - calcium: seed_single_nutritional_fact.("Calcium"), - iron: seed_single_nutritional_fact.("Iron"), - vitamin_a: seed_single_nutritional_fact.("Vitamin A"), - vitamin_c: seed_single_nutritional_fact.("Vitamin C"), - vitamin_d: seed_single_nutritional_fact.("Vitamin D") - } -end - if System.get_env("SEED_DATA") == "true" or (Code.ensure_loaded?(Mix) and Mix.env() == :dev) do - # ------------------------------------------------------------------------------ - # 2. Clear existing data (cleanup for repeated seeds in dev) - # ------------------------------------------------------------------------------ Repo.delete_all(Orders.ProductionBatchLot) Repo.delete_all(Orders.OrderItemBatchAllocation) Repo.delete_all(Orders.OrderItemLot) Repo.delete_all(Orders.OrderItem) Repo.delete_all(Orders.Order) - # Legacy Recipe resources removed; no cleanup required - # Clear BOMs and related rollups/components before products to avoid FKs Repo.delete_all(Catalog.BOMRollup) Repo.delete_all(Catalog.BOMComponent) Repo.delete_all(Catalog.LaborStep) Repo.delete_all(Catalog.BOM) - - # Clear products (after BOM cleanup) Repo.delete_all(Catalog.Product) Repo.delete_all(Inventory.Movement) Repo.delete_all(Inventory.Lot) Repo.delete_all(Inventory.MaterialNutritionalFact) Repo.delete_all(Inventory.NutritionalFact) Repo.delete_all(Inventory.MaterialAllergen) - # Purchasing domain must be cleared before materials (FK on material_id) Repo.delete_all(Inventory.PurchaseOrderItem) Repo.delete_all(Inventory.PurchaseOrder) Repo.delete_all(Inventory.Supplier) @@ -109,1394 +34,22 @@ if System.get_env("SEED_DATA") == "true" or (Code.ensure_loaded?(Mix) and Mix.en Repo.delete_all(Accounts.User) Repo.delete_all(Settings.Settings) - # ------------------------------------------------------------------------------ - # 3. Seed necessary data - # ------------------------------------------------------------------------------ - - seed_user = fn email, role -> - {:ok, user} = - Accounts.User - |> Ash.Changeset.for_create(:register_with_password, %{ - email: email, - password: "Aa123123123123", - password_confirmation: "Aa123123123123", - role: role - }) - |> Ash.create( - context: %{ - strategy: AshAuthentication.Strategy.Password, - private: %{ash_authentication?: true} - } - ) - - user - end - - seed_material = fn name, sku, unit, price, min, max -> - Ash.Seed.seed!(Inventory.Material, %{ - name: name, - sku: sku, - unit: unit, - price: Decimal.new(price), - minimum_stock: Decimal.new(min), - maximum_stock: Decimal.new(max) - }) - end - - link_material_allergen = fn material, allergen -> - Ash.Seed.seed!(Inventory.MaterialAllergen, %{ - material_id: material.id, - allergen_id: allergen.id - }) - end - - # Add a function to link materials to nutritional facts with amounts and units - link_material_nutritional_fact = fn material, nutritional_fact, amount, unit -> - Ash.Seed.seed!(Inventory.MaterialNutritionalFact, %{ - material_id: material.id, - nutritional_fact_id: nutritional_fact.id, - amount: Decimal.new(amount), - unit: unit - }) - end - - add_initial_stock = fn material, quantity -> - Ash.Seed.seed!(Inventory.Movement, %{ - material_id: material.id, - occurred_at: DateTime.utc_now(), - quantity: Decimal.new(quantity), - reason: "Initial stock" - }) - end - - # Seed a lot for a material and receive quantity into that lot - seed_lot_for = fn material, supplier, lot_code, quantity, expiry_in_days -> - lot = - Ash.Seed.seed!(Inventory.Lot, %{ - lot_code: lot_code, - material_id: material.id, - supplier_id: supplier && supplier.id, - received_at: DateTime.utc_now(), - expiry_date: Date.add(Date.utc_today(), expiry_in_days) - }) - - Ash.Seed.seed!(Inventory.Movement, %{ - material_id: material.id, - lot_id: lot.id, - occurred_at: DateTime.utc_now(), - quantity: Decimal.new(quantity), - reason: "Received lot #{lot_code}" - }) - - lot - end - - seed_product = fn name, sku, price -> - Ash.Seed.seed!(Catalog.Product, %{ - name: name, - sku: sku, - status: :active, - price: Decimal.new(price) - }) - end - - # No recipe helpers (BOM-only seeding) - - seed_bom = fn product, component_defs, labor_defs, opts -> - opts = opts || [] - - status = Keyword.get(opts, :status, :draft) - - published_at = - case Keyword.get(opts, :published_at) do - nil -> - if status == :active do - DateTime.utc_now() - end - - value -> - value - end - - components = - component_defs - |> Enum.with_index(1) - |> Enum.map(fn {attrs, position} -> - Map.put(attrs, :position, position) - end) - - labor_steps = - labor_defs - |> Enum.with_index(1) - |> Enum.map(fn {attrs, sequence} -> - attrs - |> Map.put(:sequence, sequence) - |> Map.put_new(:units_per_run, Decimal.new("1")) - end) - - Catalog.BOM - |> Ash.Changeset.for_create(:create, %{ - product_id: product.id, - name: Keyword.get(opts, :name, "#{product.name} BOM"), - status: status, - published_at: published_at, - components: components, - labor_steps: labor_steps - }) - |> Ash.create!(authorize?: false) - end - - # Purchasing helpers - seed_supplier = fn name, email -> - Ash.Seed.seed!(Inventory.Supplier, %{ - name: name, - contact_email: email - }) - end - - # Note: PurchaseOrder.create defaults status to :draft in the resource. For seeds we - # still accept a `status` argument for readability, then update the PO accordingly so - # the final state matches the scenario we want to illustrate (e.g. :ordered). - seed_purchase_order = fn supplier, status -> - po = - Ash.Seed.seed!(Inventory.PurchaseOrder, %{ - supplier_id: supplier.id, - ordered_at: DateTime.utc_now() - }) - - case status do - s when s in [:ordered, :received, :cancelled] and po.status != s -> - Ash.update!(po, %{status: s}, action: :update, authorize?: false) - - _ -> - po - end - end - - seed_purchase_order_item = fn po, material, quantity, unit_price -> - Ash.Seed.seed!(Inventory.PurchaseOrderItem, %{ - purchase_order_id: po.id, - material_id: material.id, - quantity: Decimal.new(quantity), - unit_price: Decimal.new(unit_price) - }) - end - - seed_customer = fn first_name, last_name, email, phone, address_map -> - Ash.Seed.seed!(CRM.Customer, %{ - type: :individual, - first_name: first_name, - last_name: last_name, - email: email, - phone: phone, - billing_address: address_map, - shipping_address: address_map - }) - end - - seed_order = fn customer, delivery_in_days, status, payment_status -> - Ash.Seed.seed!(Orders.Order, %{ - customer_id: customer.id, - delivery_date: DateTime.add(DateTime.utc_now(), delivery_in_days, :day), - status: status, - payment_status: payment_status - }) - end - - seed_order_item = fn order, product, quantity, status -> - Ash.Seed.seed!(Orders.OrderItem, %{ - order_id: order.id, - product_id: product.id, - quantity: Decimal.new(quantity), - unit_price: product.price, - status: status - }) - end - - # -- 3.1 Create users - _admin_user = seed_user.("test@test.com", :admin) - _staff_user = seed_user.("staff@staff.com", :staff) - _customer_user = seed_user.("customer@customer.com", :customer) - - # -- 3.2 Set up global bakery settings - Ash.Seed.seed!(Settings.Settings, %{ - currency: :USD, - tax_mode: :exclusive, - tax_rate: Decimal.new("0.10"), - offers_pickup: true, - offers_delivery: true, - lead_time_days: 1, - daily_capacity: 25, - shipping_flat: Decimal.new("5.00"), - labor_hourly_rate: Decimal.new("18.50"), - labor_overhead_percent: Decimal.new("0.15"), - retail_markup_mode: :percent, - retail_markup_value: Decimal.new("35"), - wholesale_markup_mode: :percent, - wholesale_markup_value: Decimal.new("20") - }) - - # -- 3.3 Allergen data - allergens = seed_allergens.() - - # -- 3.4 Nutritional facts data - nutritional_facts = seed_nutritional_facts.() - - # -- 3.5 Materials - materials = %{ - flour: seed_material.("All Purpose Flour", "FLOUR-001", :gram, "0.002", "5000", "20000"), - whole_wheat: seed_material.("Whole Wheat Flour", "FLOUR-002", :gram, "0.003", "3000", "15000"), - rye_flour: seed_material.("Rye Flour", "FLOUR-003", :gram, "0.004", "2000", "8000"), - gluten_free_mix: seed_material.("Gluten-Free Flour Mix", "GF-001", :gram, "0.005", "1000", "7000"), - oats: seed_material.("Rolled Oats", "OATS-001", :gram, "0.0025", "2000", "10000"), - almonds: seed_material.("Whole Almonds", "NUTS-001", :gram, "0.02", "2000", "10000"), - walnuts: seed_material.("Walnuts", "NUTS-002", :gram, "0.025", "1500", "8000"), - eggs: seed_material.("Fresh Eggs", "EGG-001", :piece, "0.15", "100", "500"), - milk: seed_material.("Whole Milk", "MILK-001", :milliliter, "0.003", "2000", "10000"), - butter: seed_material.("Butter", "DAIRY-001", :gram, "0.01", "1000", "5000"), - cream_cheese: seed_material.("Cream Cheese", "DAIRY-002", :gram, "0.015", "500", "3000"), - sugar: seed_material.("White Sugar", "SUGAR-001", :gram, "0.003", "3000", "15000"), - brown_sugar: seed_material.("Brown Sugar", "SUGAR-002", :gram, "0.004", "2000", "10000"), - chocolate: seed_material.("Dark Chocolate", "CHOC-001", :gram, "0.02", "2000", "8000"), - vanilla: seed_material.("Vanilla Extract", "FLAV-001", :milliliter, "0.15", "500", "2000"), - cinnamon: seed_material.("Ground Cinnamon", "SPICE-001", :gram, "0.006", "300", "1500"), - yeast: seed_material.("Active Dry Yeast", "YEAST-001", :gram, "0.05", "500", "2000"), - salt: seed_material.("Sea Salt", "SALT-001", :gram, "0.001", "1000", "5000") - } - - # -- 3.6 Link materials to relevant allergens - link_material_allergen.(materials.flour, allergens.gluten) - link_material_allergen.(materials.whole_wheat, allergens.gluten) - link_material_allergen.(materials.rye_flour, allergens.gluten) - link_material_allergen.(materials.gluten_free_mix, allergens.nuts) - link_material_allergen.(materials.almonds, allergens.nuts) - link_material_allergen.(materials.walnuts, allergens.nuts) - link_material_allergen.(materials.eggs, allergens.eggs) - link_material_allergen.(materials.milk, allergens.milk) - link_material_allergen.(materials.butter, allergens.milk) - link_material_allergen.(materials.cream_cheese, allergens.milk) - - # -- 3.7 Link materials to nutritional facts - # Flour - link_material_nutritional_fact.(materials.flour, nutritional_facts.calories, "350", :kcal) - link_material_nutritional_fact.(materials.flour, nutritional_facts.carbohydrates, "73", :gram) - link_material_nutritional_fact.(materials.flour, nutritional_facts.protein, "10", :gram) - link_material_nutritional_fact.(materials.flour, nutritional_facts.fat, "1", :gram) - - # Whole Wheat Flour - link_material_nutritional_fact.(materials.whole_wheat, nutritional_facts.calories, "340", :kcal) - - link_material_nutritional_fact.( - materials.whole_wheat, - nutritional_facts.carbohydrates, - "72", - :gram - ) - - link_material_nutritional_fact.(materials.whole_wheat, nutritional_facts.protein, "13", :gram) - link_material_nutritional_fact.(materials.whole_wheat, nutritional_facts.fiber, "11", :gram) - - # Almonds - link_material_nutritional_fact.(materials.almonds, nutritional_facts.calories, "580", :kcal) - link_material_nutritional_fact.(materials.almonds, nutritional_facts.fat, "50", :gram) - link_material_nutritional_fact.(materials.almonds, nutritional_facts.protein, "21", :gram) - - # Eggs - link_material_nutritional_fact.(materials.eggs, nutritional_facts.calories, "155", :kcal) - link_material_nutritional_fact.(materials.eggs, nutritional_facts.protein, "13", :gram) - link_material_nutritional_fact.(materials.eggs, nutritional_facts.fat, "11", :gram) - - # Milk - link_material_nutritional_fact.(materials.milk, nutritional_facts.calories, "42", :kcal) - link_material_nutritional_fact.(materials.milk, nutritional_facts.protein, "3.4", :gram) - link_material_nutritional_fact.(materials.milk, nutritional_facts.calcium, "125", :milligram) - - # Butter - link_material_nutritional_fact.(materials.butter, nutritional_facts.calories, "717", :kcal) - link_material_nutritional_fact.(materials.butter, nutritional_facts.fat, "81", :gram) - link_material_nutritional_fact.(materials.butter, nutritional_facts.saturated_fat, "51", :gram) - - # Chocolate - link_material_nutritional_fact.(materials.chocolate, nutritional_facts.calories, "546", :kcal) - link_material_nutritional_fact.(materials.chocolate, nutritional_facts.fat, "31", :gram) - - link_material_nutritional_fact.( - materials.chocolate, - nutritional_facts.carbohydrates, - "61", - :gram - ) - - link_material_nutritional_fact.(materials.chocolate, nutritional_facts.iron, "8", :milligram) - - # -- 3.8 Add some initial stock - Enum.each(materials, fn {_key, material} -> - add_initial_stock.(material, "5000") - end) - - # -- 3.8.1 Suppliers and Purchase Orders - suppliers = %{ - miller: seed_supplier.("Miller & Co.", "hello@miller.test"), - dairy: seed_supplier.("Fresh Dairy Ltd.", "sales@dairy.test") - } - - po1 = seed_purchase_order.(suppliers.miller, :ordered) - seed_purchase_order_item.(po1, materials.flour, "10000", "0.0018") - seed_purchase_order_item.(po1, materials.whole_wheat, "5000", "0.0027") - - po2 = seed_purchase_order.(suppliers.dairy, :ordered) - seed_purchase_order_item.(po2, materials.butter, "2000", "0.009") - seed_purchase_order_item.(po2, materials.milk, "5000", "0.0025") - - # -- 3.8.2 Seed example lots for FIFO/traceability testing - # Dairy lots (perishables) - _milk_lot1 = - seed_lot_for.( - materials.milk, - suppliers.dairy, - "LOT-MILK-#{Date.to_string(Date.utc_today())}-001", - "1500", - 7 - ) - - _milk_lot2 = - seed_lot_for.( - materials.milk, - suppliers.dairy, - "LOT-MILK-#{Date.to_string(Date.utc_today())}-002", - "1200", - 14 - ) - - _butter_lot = - seed_lot_for.( - materials.butter, - suppliers.dairy, - "LOT-BUTTER-#{Date.to_string(Date.utc_today())}-001", - "800", - 60 - ) - - # Flour lot (non-perishable) - _flour_lot = - seed_lot_for.( - materials.flour, - suppliers.miller, - "LOT-FLOUR-#{Date.to_string(Date.utc_today())}-A", - "2000", - 365 - ) - - # -- 3.9 Seed products - products = %{ - almond_cookies: seed_product.("Almond Cookies", "COOK-001", "3.99"), - choc_cake: seed_product.("Chocolate Cake", "CAKE-001", "15.99"), - bread: seed_product.("Artisan Bread", "BREAD-001", "4.99"), - muffins: seed_product.("Blueberry Muffins", "MUF-001", "2.99"), - croissants: seed_product.("Butter Croissants", "PAST-001", "2.50"), - gf_cupcakes: seed_product.("Gluten-Free Cupcakes", "CUP-001", "3.49"), - rye_loaf: seed_product.("Rye Loaf Bread", "BREAD-002", "5.49"), - carrot_cake: seed_product.("Carrot Cake", "CAKE-002", "12.99"), - oatmeal_cookies: seed_product.("Oatmeal Cookies", "COOK-002", "3.49"), - cheese_danish: seed_product.("Cheese Danish", "PAST-002", "2.99") - } - - # Set product availability and per-day capacity to try the feature - update_product = fn product, attrs -> - product |> Ash.Changeset.for_update(:update, attrs) |> Ash.update!(authorize?: false) - end - - products = %{ - almond_cookies: update_product.(products.almond_cookies, %{max_daily_quantity: 200}), - choc_cake: update_product.(products.choc_cake, %{max_daily_quantity: 20}), - bread: update_product.(products.bread, %{max_daily_quantity: 150}), - muffins: update_product.(products.muffins, %{max_daily_quantity: 120}), - croissants: - update_product.(products.croissants, %{ - max_daily_quantity: 80, - selling_availability: :preorder - }), - gf_cupcakes: update_product.(products.gf_cupcakes, %{max_daily_quantity: 60}), - rye_loaf: update_product.(products.rye_loaf, %{max_daily_quantity: 50}), - carrot_cake: update_product.(products.carrot_cake, %{selling_availability: :off, max_daily_quantity: 0}), - oatmeal_cookies: update_product.(products.oatmeal_cookies, %{max_daily_quantity: 200}), - cheese_danish: update_product.(products.cheese_danish, %{max_daily_quantity: 100}) - } - - # -- 3.12 Seed customers - customers = %{ - john: - seed_customer.( - "John", - "Doe", - "john@example.com", - "1234567890", - %{ - street: "123 Main St", - city: "Springfield", - state: "IL", - zip: "12345", - country: "USA" - } - ), - jane: - seed_customer.( - "Jane", - "Smith", - "jane@example.com", - "9876543210", - %{ - street: "456 Oak Ave", - city: "Portland", - state: "OR", - zip: "97201", - country: "USA" - } - ), - bob: - seed_customer.( - "Bob", - "Johnson", - "bob@example.com", - "5551234567", - %{ - street: "789 Pine St", - city: "Seattle", - state: "WA", - zip: "98101", - country: "USA" - } - ), - alice: - seed_customer.( - "Alice", - "Anderson", - "alice@example.com", - "2225557777", - %{ - street: "101 Apple Rd", - city: "Denver", - state: "CO", - zip: "80203", - country: "USA" - } - ), - michael: - seed_customer.( - "Michael", - "Brown", - "michael@example.com", - "1112223333", - %{ - street: "202 Banana Blvd", - city: "Phoenix", - state: "AZ", - zip: "85001", - country: "USA" - } - ), - grace: - seed_customer.( - "Grace", - "Thomas", - "grace@example.com", - "4445556666", - %{ - street: "350 Elm St", - city: "Austin", - state: "TX", - zip: "73301", - country: "USA" - } - ), - taylor: - seed_customer.( - "Taylor", - "Evans", - "taylor@example.com", - "7778889999", - %{ - street: "999 Maple Ave", - city: "Boston", - state: "MA", - zip: "02215", - country: "USA" - } - ), - emily: - seed_customer.( - "Emily", - "Clark", - "emily@example.com", - "6667778888", - %{ - street: "202 Cedar St", - city: "Chicago", - state: "IL", - zip: "60601", - country: "USA" - } - ) - } - - # -- 3.13 Seed demo orders for Bread (today) and create an open batch with allocations - bread_order1 = - Ash.Seed.seed!(Orders.Order, %{ - customer_id: customers.john.id, - delivery_date: DateTime.utc_now(), - status: :confirmed, - payment_status: :pending - }) - - bread_item1 = - Ash.Seed.seed!(Orders.OrderItem, %{ - order_id: bread_order1.id, - product_id: products.bread.id, - quantity: Decimal.new("10"), - unit_price: products.bread.price, - status: :todo - }) - - bread_order2 = - Ash.Seed.seed!(Orders.Order, %{ - customer_id: customers.jane.id, - delivery_date: DateTime.utc_now(), - status: :confirmed, - payment_status: :pending - }) - - bread_item2 = - Ash.Seed.seed!(Orders.OrderItem, %{ - order_id: bread_order2.id, - product_id: products.bread.id, - quantity: Decimal.new("5"), - unit_price: products.bread.price, - status: :todo - }) - - demo_batch_code = - "B-" <> Calendar.strftime(Date.utc_today(), "%Y%m%d") <> "-" <> products.bread.sku <> "-DEV" - - bread_batch = - Ash.Seed.seed!(Orders.ProductionBatch, %{ - batch_code: demo_batch_code, - product_id: products.bread.id, - planned_qty: Decimal.new("15"), - produced_qty: Decimal.new("0"), - scrap_qty: Decimal.new("0"), - status: :open, - components_map: %{} - }) - - _ = - Ash.Seed.seed!(Orders.OrderItemBatchAllocation, %{ - production_batch_id: bread_batch.id, - order_item_id: bread_item1.id, - planned_qty: Decimal.new("10"), - completed_qty: Decimal.new("0") - }) - - _ = - Ash.Seed.seed!(Orders.OrderItemBatchAllocation, %{ - production_batch_id: bread_batch.id, - order_item_id: bread_item2.id, - planned_qty: Decimal.new("5"), - completed_qty: Decimal.new("0") - }) - - # ------------------------------------------------------------------------------ - # ✨ Add-on: richer scenarios and edge cases - # ------------------------------------------------------------------------------ - - # Helper for non-initial stock movements - adjust_stock = fn material, quantity, reason, days_from_now -> - Ash.Seed.seed!(Inventory.Movement, %{ - material_id: material.id, - occurred_at: DateTime.add(DateTime.utc_now(), days_from_now, :day), - quantity: Decimal.new(quantity), - reason: reason - }) - end - - # A) New materials to unlock more recipes - new_materials = %{ - blueberries: seed_material.("Blueberries", "FRUIT-001", :gram, "0.010", "500", "3000"), - sesame_seeds: seed_material.("Sesame Seeds", "SEED-001", :gram, "0.012", "500", "3000"), - peanut_butter: seed_material.("Peanut Butter", "PB-001", :gram, "0.015", "500", "3000") - } - - materials = Map.merge(materials, new_materials) - - # Link new materials to allergens - link_material_allergen.(materials.sesame_seeds, allergens.sesame) - link_material_allergen.(materials.peanut_butter, allergens.peanuts) - - # Nutritional facts for the new materials - link_material_nutritional_fact.(materials.blueberries, nutritional_facts.calories, "57", :kcal) - link_material_nutritional_fact.(materials.blueberries, nutritional_facts.fiber, "2.4", :gram) - - link_material_nutritional_fact.( - materials.sesame_seeds, - nutritional_facts.calories, - "573", - :kcal - ) - - link_material_nutritional_fact.(materials.sesame_seeds, nutritional_facts.fat, "50", :gram) - - link_material_nutritional_fact.( - materials.peanut_butter, - nutritional_facts.calories, - "588", - :kcal - ) - - link_material_nutritional_fact.(materials.peanut_butter, nutritional_facts.fat, "50", :gram) - link_material_nutritional_fact.(materials.peanut_butter, nutritional_facts.protein, "25", :gram) - - # Stock for new materials - Enum.each(new_materials, fn {_k, m} -> add_initial_stock.(m, "3000") end) - - # C) New products - new_products = %{ - sesame_bagel: seed_product.("Sesame Bagel", "BAGEL-001", "2.25"), - pb_cookies: seed_product.("Peanut Butter Cookies", "COOK-003", "3.79") - } - - products = Map.merge(products, new_products) - - # Availability and caps for new products - products = - products - |> Map.put(:sesame_bagel, update_product.(products.sesame_bagel, %{max_daily_quantity: 120})) - |> Map.put(:pb_cookies, update_product.(products.pb_cookies, %{max_daily_quantity: 180})) - - # -- 3.11 Seed BOMs (80%+ active coverage, no drafts) - _almond_cookies_bom = - seed_bom.( - products.almond_cookies, - [ - %{ - component_type: :material, - material_id: materials.flour.id, - quantity: Decimal.new("50") - }, - %{ - component_type: :material, - material_id: materials.almonds.id, - quantity: Decimal.new("25") - }, - %{ - component_type: :material, - material_id: materials.sugar.id, - quantity: Decimal.new("30") - }, - %{ - component_type: :material, - material_id: materials.butter.id, - quantity: Decimal.new("25"), - waste_percent: Decimal.new("0.03") - }, - %{component_type: :material, material_id: materials.eggs.id, quantity: Decimal.new("1")} - ], - [ - %{ - name: "Prep dough", - duration_minutes: Decimal.new("10"), - units_per_run: Decimal.new("48") - }, - %{ - name: "Sheet & cut", - duration_minutes: Decimal.new("8"), - units_per_run: Decimal.new("48") - }, - %{ - name: "Bake trays", - duration_minutes: Decimal.new("12"), - rate_override: Decimal.new("25"), - units_per_run: Decimal.new("48") - } - ], - status: :active, - name: "Almond Cookies BOM v1" - ) - - _almond_cookies_archived = - seed_bom.( - products.almond_cookies, - [ - %{ - component_type: :material, - material_id: materials.flour.id, - quantity: Decimal.new("48") - }, - %{ - component_type: :material, - material_id: materials.almonds.id, - quantity: Decimal.new("27") - }, - %{ - component_type: :material, - material_id: materials.brown_sugar.id, - quantity: Decimal.new("28") - }, - %{ - component_type: :material, - material_id: materials.butter.id, - quantity: Decimal.new("26"), - waste_percent: Decimal.new("0.05") - }, - %{component_type: :material, material_id: materials.eggs.id, quantity: Decimal.new("1")} - ], - [ - %{ - name: "Prep dough", - duration_minutes: Decimal.new("11"), - units_per_run: Decimal.new("48") - }, - %{ - name: "Bake tests", - duration_minutes: Decimal.new("14"), - units_per_run: Decimal.new("48") - } - ], - status: :archived, - name: "Almond Cookies BOM R&D" - ) - - _choc_cake_bom = - seed_bom.( - products.choc_cake, - [ - %{ - component_type: :material, - material_id: materials.flour.id, - quantity: Decimal.new("220") - }, - %{ - component_type: :material, - material_id: materials.sugar.id, - quantity: Decimal.new("180") - }, - %{component_type: :material, material_id: materials.eggs.id, quantity: Decimal.new("4")}, - %{ - component_type: :material, - material_id: materials.butter.id, - quantity: Decimal.new("90") - }, - %{ - component_type: :material, - material_id: materials.chocolate.id, - quantity: Decimal.new("120") - }, - %{component_type: :material, material_id: materials.milk.id, quantity: Decimal.new("140")} - ], - [ - %{name: "Mix batter", duration_minutes: Decimal.new("15")}, - %{name: "Bake layers", duration_minutes: Decimal.new("40")}, - %{name: "Frost & finish", duration_minutes: Decimal.new("12")} - ], - status: :active, - name: "Chocolate Cake BOM v1" - ) - - _bread_bom = - seed_bom.( - products.bread, - [ - %{ - component_type: :material, - material_id: materials.flour.id, - quantity: Decimal.new("500"), - waste_percent: Decimal.new("0.05") - }, - %{component_type: :material, material_id: materials.yeast.id, quantity: Decimal.new("7")}, - %{component_type: :material, material_id: materials.salt.id, quantity: Decimal.new("10")} - ], - [ - %{ - name: "Mix & knead", - duration_minutes: Decimal.new("15"), - units_per_run: Decimal.new("12") - }, - %{ - name: "Bulk proof", - duration_minutes: Decimal.new("60"), - rate_override: Decimal.new("18") - }, - %{ - name: "Bake loaves", - duration_minutes: Decimal.new("35"), - units_per_run: Decimal.new("12") - } - ], - status: :active, - name: "Artisan Bread BOM v1" - ) - - _muffins_bom = - seed_bom.( - products.muffins, - [ - %{ - component_type: :material, - material_id: materials.flour.id, - quantity: Decimal.new("120") - }, - %{ - component_type: :material, - material_id: materials.sugar.id, - quantity: Decimal.new("80") - }, - %{ - component_type: :material, - material_id: materials.butter.id, - quantity: Decimal.new("60") - }, - %{component_type: :material, material_id: materials.eggs.id, quantity: Decimal.new("2")}, - %{ - component_type: :material, - material_id: materials.milk.id, - quantity: Decimal.new("100") - }, - %{ - component_type: :material, - material_id: materials.blueberries.id, - quantity: Decimal.new("90") - } - ], - [ - %{ - name: "Mix batter", - duration_minutes: Decimal.new("8"), - units_per_run: Decimal.new("24") - }, - %{ - name: "Fill tins", - duration_minutes: Decimal.new("4"), - units_per_run: Decimal.new("24") - }, - %{name: "Bake", duration_minutes: Decimal.new("18"), units_per_run: Decimal.new("24")} - ], - status: :active, - name: "Blueberry Muffins BOM v1" - ) - - _croissants_bom = - seed_bom.( - products.croissants, - [ - %{ - component_type: :material, - material_id: materials.flour.id, - quantity: Decimal.new("300") - }, - %{ - component_type: :material, - material_id: materials.butter.id, - quantity: Decimal.new("200"), - waste_percent: Decimal.new("0.08") - }, - %{component_type: :material, material_id: materials.yeast.id, quantity: Decimal.new("5")}, - %{component_type: :material, material_id: materials.milk.id, quantity: Decimal.new("100")} - ], - [ - %{ - name: "Laminate butter", - duration_minutes: Decimal.new("45"), - units_per_run: Decimal.new("20") - }, - %{name: "Proof", duration_minutes: Decimal.new("90"), units_per_run: Decimal.new("20")}, - %{name: "Bake", duration_minutes: Decimal.new("20"), units_per_run: Decimal.new("12")} - ], - status: :active, - name: "Croissant BOM v1" - ) - - _gf_cupcakes_bom = - seed_bom.( - products.gf_cupcakes, - [ - %{ - component_type: :material, - material_id: materials.gluten_free_mix.id, - quantity: Decimal.new("140") - }, - %{ - component_type: :material, - material_id: materials.sugar.id, - quantity: Decimal.new("90") - }, - %{component_type: :material, material_id: materials.eggs.id, quantity: Decimal.new("2")}, - %{ - component_type: :material, - material_id: materials.butter.id, - quantity: Decimal.new("60") - }, - %{ - component_type: :material, - material_id: materials.vanilla.id, - quantity: Decimal.new("8") - } - ], - [ - %{ - name: "Mix batter", - duration_minutes: Decimal.new("9"), - units_per_run: Decimal.new("12") - }, - %{name: "Pipe", duration_minutes: Decimal.new("5"), units_per_run: Decimal.new("12")}, - %{name: "Bake", duration_minutes: Decimal.new("20"), units_per_run: Decimal.new("20")} - ], - status: :active, - name: "Gluten-Free Cupcakes BOM v1" - ) - - _rye_loaf_bom = - seed_bom.( - products.rye_loaf, - [ - %{ - component_type: :material, - material_id: materials.rye_flour.id, - quantity: Decimal.new("350") - }, - %{ - component_type: :material, - material_id: materials.whole_wheat.id, - quantity: Decimal.new("150") - }, - %{component_type: :material, material_id: materials.yeast.id, quantity: Decimal.new("6")}, - %{component_type: :material, material_id: materials.salt.id, quantity: Decimal.new("9")} - ], - [ - %{ - name: "Mix dough", - duration_minutes: Decimal.new("14"), - units_per_run: Decimal.new("8") - }, - %{name: "Proof", duration_minutes: Decimal.new("55"), units_per_run: Decimal.new("8")}, - %{name: "Bake", duration_minutes: Decimal.new("38"), units_per_run: Decimal.new("8")} - ], - status: :active, - name: "Rye Loaf BOM v1" - ) - - _carrot_cake_bom = - seed_bom.( - products.carrot_cake, - [ - %{ - component_type: :material, - material_id: materials.flour.id, - quantity: Decimal.new("200") - }, - %{ - component_type: :material, - material_id: materials.sugar.id, - quantity: Decimal.new("150") - }, - %{component_type: :material, material_id: materials.eggs.id, quantity: Decimal.new("3")}, - %{ - component_type: :material, - material_id: materials.butter.id, - quantity: Decimal.new("75") - }, - %{ - component_type: :material, - material_id: materials.cinnamon.id, - quantity: Decimal.new("5") - }, - %{ - component_type: :product, - product_id: products.almond_cookies.id, - quantity: Decimal.new("0.2") - } - ], - [ - %{ - name: "Mix batter", - duration_minutes: Decimal.new("12"), - units_per_run: Decimal.new("1") - }, - %{ - name: "Bake layers", - duration_minutes: Decimal.new("45"), - units_per_run: Decimal.new("1") - }, - %{ - name: "Frost & decorate", - duration_minutes: Decimal.new("10"), - rate_override: Decimal.new("22"), - units_per_run: Decimal.new("1") - } - ], - status: :active, - name: "Carrot Cake BOM v1" - ) - - _oatmeal_cookies_bom = - seed_bom.( - products.oatmeal_cookies, - [ - %{ - component_type: :material, - material_id: materials.oats.id, - quantity: Decimal.new("120") - }, - %{ - component_type: :material, - material_id: materials.flour.id, - quantity: Decimal.new("80") - }, - %{ - component_type: :material, - material_id: materials.brown_sugar.id, - quantity: Decimal.new("70") - }, - %{ - component_type: :material, - material_id: materials.butter.id, - quantity: Decimal.new("60") - }, - %{component_type: :material, material_id: materials.eggs.id, quantity: Decimal.new("1")}, - %{ - component_type: :material, - material_id: materials.cinnamon.id, - quantity: Decimal.new("4") - } - ], - [ - %{ - name: "Cream butter & sugar", - duration_minutes: Decimal.new("6"), - units_per_run: Decimal.new("48") - }, - %{ - name: "Fold dry ingredients", - duration_minutes: Decimal.new("5"), - units_per_run: Decimal.new("48") - }, - %{name: "Bake", duration_minutes: Decimal.new("16"), units_per_run: Decimal.new("48")} - ], - status: :active, - name: "Oatmeal Cookies BOM v1" - ) - - _cheese_danish_bom = - seed_bom.( - products.cheese_danish, - [ - %{ - component_type: :material, - material_id: materials.flour.id, - quantity: Decimal.new("220") - }, - %{ - component_type: :material, - material_id: materials.butter.id, - quantity: Decimal.new("160") - }, - %{ - component_type: :material, - material_id: materials.cream_cheese.id, - quantity: Decimal.new("100") - }, - %{ - component_type: :material, - material_id: materials.sugar.id, - quantity: Decimal.new("60") - }, - %{component_type: :material, material_id: materials.eggs.id, quantity: Decimal.new("1")} - ], - [ - %{ - name: "Laminate dough", - duration_minutes: Decimal.new("35"), - units_per_run: Decimal.new("24") - }, - %{ - name: "Prepare filling", - duration_minutes: Decimal.new("8"), - units_per_run: Decimal.new("24") - }, - %{name: "Bake", duration_minutes: Decimal.new("18"), units_per_run: Decimal.new("24")} - ], - status: :active, - name: "Cheese Danish BOM v1" - ) - - # Leave some products without a BOM to represent newly onboarded catalog items - _products_without_boms = [:sesame_bagel, :pb_cookies] - - # D) Inventory edge cases: spoilage, breakage, low stock and receipts - adjust_stock.(materials.milk, "-1000", "Spoilage – fridge failure", -1) - adjust_stock.(materials.eggs, "-12", "Breakage – dropped tray", 0) - adjust_stock.(materials.yeast, "-400", "Use in production surge", -2) - adjust_stock.(materials.yeast, "-300", "Use in production surge", -1) - - supplier_baker = seed_supplier.("Baker Supplies", "orders@bakersup.test") - po3 = seed_purchase_order.(supplier_baker, :ordered) - seed_purchase_order_item.(po3, materials.butter, "1500", "0.0095") - seed_purchase_order_item.(po3, materials.yeast, "1200", "0.048") - - # Receive stock today - adjust_stock.(materials.butter, "1500", "PO #{po3.id} receipt", 0) - adjust_stock.(materials.yeast, "1200", "PO #{po3.id} receipt", 0) - - # E) Capacity stress test for tomorrow - cap1 = seed_order.(customers.john, 1, :confirmed, :pending) - seed_order_item.(cap1, products.croissants, "60", :todo) - seed_order_item.(cap1, products.muffins, "40", :todo) - - cap2 = seed_order.(customers.jane, 1, :confirmed, :pending) - seed_order_item.(cap2, products.sesame_bagel, "50", :todo) - seed_order_item.(cap2, products.pb_cookies, "30", :todo) - - # F) Availability off edge case - off_case = seed_order.(customers.michael, 9, :unconfirmed, :pending) - seed_order_item.(off_case, products.carrot_cake, "1", :todo) - - # G) Long-range history - old_q = seed_order.(customers.alice, -90, :delivered, :paid) - seed_order_item.(old_q, products.bread, "2", :done) - seed_order_item.(old_q, products.oatmeal_cookies, "18", :done) - - old_h = seed_order.(customers.bob, -180, :delivered, :paid) - seed_order_item.(old_h, products.rye_loaf, "3", :done) - seed_order_item.(old_h, products.choc_cake, "1", :done) - - # H) Allergen-heavy event order - allergen_party = seed_order.(customers.grace, 6, :unconfirmed, :pending) - # tree nuts - seed_order_item.(allergen_party, products.almond_cookies, "24", :todo) - # peanuts - seed_order_item.(allergen_party, products.pb_cookies, "24", :todo) - # sesame - seed_order_item.(allergen_party, products.sesame_bagel, "24", :todo) - - # ------------------------------------------------------------------------------ - # 4. Create orders for these customers (simulate real bakery operations) - # ------------------------------------------------------------------------------ - # ----------------------------- - # PAST WEEK (Days -7 to -1) - # ----------------------------- - - # Last Week - Monday (Day -7) - order1 = seed_order.(customers.john, -7, :delivered, :paid) - seed_order_item.(order1, products.bread, "2", :done) - seed_order_item.(order1, products.muffins, "6", :done) - seed_order_item.(order1, products.croissants, "4", :done) - - order2 = seed_order.(customers.jane, -7, :delivered, :paid) - seed_order_item.(order2, products.choc_cake, "1", :done) - seed_order_item.(order2, products.gf_cupcakes, "8", :done) - - order3 = seed_order.(customers.michael, -7, :delivered, :paid) - seed_order_item.(order3, products.oatmeal_cookies, "12", :done) - seed_order_item.(order3, products.bread, "1", :done) - seed_order_item.(order3, products.rye_loaf, "1", :done) - - # Last Week - Tuesday (Day -6) - order4 = seed_order.(customers.alice, -6, :delivered, :paid) - seed_order_item.(order4, products.bread, "3", :done) - seed_order_item.(order4, products.carrot_cake, "1", :done) - - order5 = seed_order.(customers.grace, -6, :delivered, :paid) - seed_order_item.(order5, products.cheese_danish, "5", :done) - seed_order_item.(order5, products.croissants, "6", :done) - seed_order_item.(order5, products.almond_cookies, "10", :done) - - order6 = seed_order.(customers.bob, -6, :cancelled, :refunded) - seed_order_item.(order6, products.choc_cake, "1", :done) - - # Last Week - Thursday (Day -4) - order7 = seed_order.(customers.taylor, -4, :delivered, :paid) - seed_order_item.(order7, products.choc_cake, "2", :done) - seed_order_item.(order7, products.bread, "2", :done) - - order8 = seed_order.(customers.emily, -4, :delivered, :paid) - seed_order_item.(order8, products.rye_loaf, "1", :done) - seed_order_item.(order8, products.croissants, "12", :done) - seed_order_item.(order8, products.muffins, "4", :done) - - # Last Weekend - Saturday (Day -2) - order9 = seed_order.(customers.john, -2, :delivered, :paid) - seed_order_item.(order9, products.choc_cake, "1", :done) - seed_order_item.(order9, products.muffins, "12", :done) - seed_order_item.(order9, products.bread, "2", :done) - seed_order_item.(order9, products.croissants, "8", :done) - - order10 = seed_order.(customers.michael, -2, :delivered, :paid) - seed_order_item.(order10, products.choc_cake, "1", :done) - seed_order_item.(order10, products.gf_cupcakes, "12", :done) - - order11 = seed_order.(customers.grace, -2, :delivered, :paid) - seed_order_item.(order11, products.carrot_cake, "1", :done) - seed_order_item.(order11, products.oatmeal_cookies, "24", :done) - - order12 = seed_order.(customers.jane, -2, :delivered, :paid) - seed_order_item.(order12, products.choc_cake, "1", :done) - seed_order_item.(order12, products.almond_cookies, "15", :done) - - # ----------------------------- - # CURRENT WEEK (Days 0 to 7) - # ----------------------------- - - # Today (Day 0) - order13 = seed_order.(customers.alice, 0, :completed, :paid) - seed_order_item.(order13, products.bread, "2", :done) - seed_order_item.(order13, products.croissants, "6", :done) - - order14 = seed_order.(customers.bob, 0, :completed, :paid) - seed_order_item.(order14, products.carrot_cake, "1", :done) - seed_order_item.(order14, products.gf_cupcakes, "6", :done) - seed_order_item.(order14, products.rye_loaf, "1", :done) - - order15 = seed_order.(customers.taylor, 0, :ready, :pending) - seed_order_item.(order15, products.bread, "3", :done) - seed_order_item.(order15, products.muffins, "8", :in_progress) - - order16 = seed_order.(customers.emily, 0, :ready, :pending) - seed_order_item.(order16, products.choc_cake, "1", :done) - seed_order_item.(order16, products.cheese_danish, "8", :in_progress) - - # Tomorrow (Day 1) - order17 = seed_order.(customers.john, 1, :confirmed, :pending) - seed_order_item.(order17, products.bread, "2", :in_progress) - seed_order_item.(order17, products.croissants, "4", :todo) - - order18 = seed_order.(customers.jane, 1, :confirmed, :pending) - seed_order_item.(order18, products.bread, "1", :done) - seed_order_item.(order18, products.almond_cookies, "10", :todo) - - order19 = seed_order.(customers.michael, 1, :confirmed, :pending) - seed_order_item.(order19, products.bread, "2", :in_progress) - seed_order_item.(order19, products.oatmeal_cookies, "15", :done) - seed_order_item.(order19, products.muffins, "6", :todo) - - # This Week - Wednesday (Day 3) - order20 = seed_order.(customers.bob, 3, :confirmed, :pending) - seed_order_item.(order20, products.choc_cake, "1", :in_progress) - seed_order_item.(order20, products.rye_loaf, "2", :todo) - - order21 = seed_order.(customers.grace, 3, :confirmed, :pending) - seed_order_item.(order21, products.choc_cake, "1", :done) - seed_order_item.(order21, products.croissants, "8", :in_progress) - seed_order_item.(order21, products.almond_cookies, "12", :todo) - - order22 = seed_order.(customers.alice, 3, :confirmed, :pending) - seed_order_item.(order22, products.choc_cake, "1", :done) - seed_order_item.(order22, products.muffins, "4", :in_progress) - - # This Week - Friday (Day 5) - order23 = seed_order.(customers.taylor, 5, :unconfirmed, :pending) - seed_order_item.(order23, products.carrot_cake, "2", :done) - seed_order_item.(order23, products.bread, "3", :in_progress) - seed_order_item.(order23, products.croissants, "6", :todo) - - order24 = seed_order.(customers.emily, 5, :unconfirmed, :pending) - seed_order_item.(order24, products.carrot_cake, "1", :in_progress) - seed_order_item.(order24, products.gf_cupcakes, "12", :todo) - - # Weekend Event Orders (Day 6-7) - order25 = seed_order.(customers.john, 6, :unconfirmed, :pending) - seed_order_item.(order25, products.carrot_cake, "1", :done) - seed_order_item.(order25, products.cheese_danish, "12", :in_progress) - seed_order_item.(order25, products.bread, "5", :in_progress) - seed_order_item.(order25, products.croissants, "12", :todo) - seed_order_item.(order25, products.muffins, "24", :todo) - - order26 = seed_order.(customers.jane, 6, :unconfirmed, :pending) - seed_order_item.(order26, products.carrot_cake, "1", :done) - seed_order_item.(order26, products.oatmeal_cookies, "20", :in_progress) - - order27 = seed_order.(customers.michael, 7, :unconfirmed, :pending) - seed_order_item.(order27, products.choc_cake, "2", :done) - seed_order_item.(order27, products.gf_cupcakes, "15", :in_progress) - seed_order_item.(order27, products.rye_loaf, "3", :todo) - - # ----------------------------- - # NEXT WEEK (Days 8 to 14) - # ----------------------------- - - # Next Week - Monday (Day 8) - order28 = seed_order.(customers.alice, 8, :unconfirmed, :pending) - seed_order_item.(order28, products.bread, "2", :todo) - seed_order_item.(order28, products.croissants, "6", :todo) - - order29 = seed_order.(customers.bob, 8, :unconfirmed, :pending) - seed_order_item.(order29, products.choc_cake, "1", :todo) - seed_order_item.(order29, products.muffins, "6", :todo) - - # Next Week - Tuesday (Day 9) - order30 = seed_order.(customers.grace, 9, :unconfirmed, :pending) - seed_order_item.(order30, products.cheese_danish, "10", :todo) - seed_order_item.(order30, products.rye_loaf, "2", :todo) - - order31 = seed_order.(customers.taylor, 9, :unconfirmed, :pending) - seed_order_item.(order31, products.bread, "3", :todo) - seed_order_item.(order31, products.almond_cookies, "15", :todo) - seed_order_item.(order31, products.croissants, "8", :todo) - - # Office Party Orders (Day 10) - order32 = seed_order.(customers.emily, 10, :unconfirmed, :pending) - seed_order_item.(order32, products.oatmeal_cookies, "30", :todo) - seed_order_item.(order32, products.croissants, "24", :todo) - seed_order_item.(order32, products.muffins, "18", :todo) - - order33 = seed_order.(customers.john, 10, :unconfirmed, :pending) - seed_order_item.(order33, products.gf_cupcakes, "12", :todo) - seed_order_item.(order33, products.cheese_danish, "15", :todo) - - # Next Week - Friday (Day 12) - order34 = seed_order.(customers.jane, 12, :unconfirmed, :pending) - seed_order_item.(order34, products.carrot_cake, "1", :todo) - seed_order_item.(order34, products.bread, "2", :todo) - - order35 = seed_order.(customers.michael, 12, :unconfirmed, :pending) - seed_order_item.(order35, products.choc_cake, "1", :todo) - seed_order_item.(order35, products.almond_cookies, "12", :todo) - seed_order_item.(order35, products.rye_loaf, "1", :todo) - - # Weekend Event (Day 13-14) - order36 = seed_order.(customers.bob, 13, :unconfirmed, :pending) - seed_order_item.(order36, products.choc_cake, "2", :todo) - seed_order_item.(order36, products.carrot_cake, "1", :todo) - seed_order_item.(order36, products.bread, "4", :todo) - seed_order_item.(order36, products.muffins, "12", :todo) - - order37 = seed_order.(customers.alice, 14, :unconfirmed, :pending) - seed_order_item.(order37, products.croissants, "18", :todo) - seed_order_item.(order37, products.oatmeal_cookies, "24", :todo) - - order38 = seed_order.(customers.grace, 14, :unconfirmed, :pending) - seed_order_item.(order38, products.gf_cupcakes, "12", :todo) - seed_order_item.(order38, products.cheese_danish, "10", :todo) - - # -- 5. Recalculate persisted order totals now that all items exist - for order <- Orders.list_orders!(%{}, load: [:items]) do - order - |> Ash.Changeset.for_update(:update, %{}) - |> Ash.update!() - end - - # -- 6. Backfill: ensure completed items have batch_code/costs - # Some completed items may have been created without triggering the status transition - # hook. Toggle status to re-run costing & batch assignment. - missing_batch_items = - Craftplan.Orders.OrderItem - |> Ash.Query.new() - |> Ash.Query.filter(status == :done and is_nil(batch_code)) - |> Ash.read!(authorize?: false) - - Enum.each(missing_batch_items, fn item -> - # Toggle to in_progress then back to done to drive AssignBatchCodeAndCost - _ = Orders.update_item(item, %{status: :in_progress}, actor: nil) - _ = Orders.update_item(item, %{status: :done}, actor: nil) - end) - - IO.puts("Done!") -else - IO.puts("Seeds are only allowed in the dev environment.") + IO.puts("Database Truncated!") end + +Ash.Seed.seed!(Settings.Settings, %{ + currency: :USD, + tax_mode: :exclusive, + tax_rate: Decimal.new("0.10"), + offers_pickup: true, + offers_delivery: true, + lead_time_days: 1, + daily_capacity: 25, + shipping_flat: Decimal.new("5.00"), + labor_hourly_rate: Decimal.new("18.50"), + labor_overhead_percent: Decimal.new("0.15"), + retail_markup_mode: :percent, + retail_markup_value: Decimal.new("35"), + wholesale_markup_mode: :percent, + wholesale_markup_value: Decimal.new("20") +}) diff --git a/priv/repo/seeds/20260317100140_add_accounts.exs b/priv/repo/seeds/20260317100140_add_accounts.exs new file mode 100644 index 00000000..3d2e67c2 --- /dev/null +++ b/priv/repo/seeds/20260317100140_add_accounts.exs @@ -0,0 +1,28 @@ +defmodule Craftplan.Repo.Seeds.AddAccounts do + @moduledoc false + use Craftplan.Seed + + require Ash.Query + + envs([:dev, :prod]) + + @params [{"staff@test.com", :staff}, {"customer@test.com", :customer}] + + def up(_repo, _) do + Enum.each(@params, fn {email, role} -> + Craftplan.Accounts.User + |> Ash.Changeset.for_create(:register_with_password, %{ + email: email, + password: "Aa123123123123", + password_confirmation: "Aa123123123123", + role: role + }) + |> Ash.create( + context: %{ + strategy: AshAuthentication.Strategy.Password, + private: %{ash_authentication?: true} + } + ) + end) + end +end diff --git a/priv/repo/seeds/20260317100943_add_materials.exs b/priv/repo/seeds/20260317100943_add_materials.exs new file mode 100644 index 00000000..8e375406 --- /dev/null +++ b/priv/repo/seeds/20260317100943_add_materials.exs @@ -0,0 +1,42 @@ +defmodule Craftplan.Repo.Seeds.AddMaterials do + @moduledoc false + use Craftplan.Seed + + envs([:dev, :prod]) + + def up(_repo, _) do + params = [ + ["All Purpose Flour", "FLOUR-001", :gram, "0.002", "5000", "20000"], + ["Whole Wheat Flour", "FLOUR-002", :gram, "0.003", "3000", "15000"], + ["Rye Flour", "FLOUR-003", :gram, "0.004", "2000", "8000"], + ["Gluten-Free Flour Mix", "GF-001", :gram, "0.005", "1000", "7000"], + ["Gluten-Free Flour Mix", "GF-001", :gram, "0.005", "1000", "7000"], + ["Whole Almonds", "NUTS-001", :gram, "0.02", "2000", "10000"], + ["Walnuts", "NUTS-002", :gram, "0.025", "1500", "8000"], + ["Fresh Eggs", "EGG-001", :piece, "0.15", "100", "500"], + ["Whole Milk", "MILK-001", :milliliter, "0.003", "2000", "10000"], + ["Butter", "DAIRY-001", :gram, "0.01", "1000", "5000"], + ["Cream Cheese", "DAIRY-002", :gram, "0.015", "500", "3000"], + ["White Sugar", "SUGAR-001", :gram, "0.003", "3000", "15000"], + ["Brown Sugar", "SUGAR-002", :gram, "0.004", "2000", "10000"], + ["Dark Chocolate", "CHOC-001", :gram, "0.02", "2000", "8000"], + ["Vanilla Extract", "FLAV-001", :milliliter, "0.15", "500", "2000"], + ["Ground Cinnamon", "SPICE-001", :gram, "0.006", "300", "1500"], + ["Active Dry Yeast", "YEAST-001", :gram, "0.05", "500", "2000"], + ["Sea Salt", "SALT-001", :gram, "0.001", "1000", "5000"] + ] + + Enum.each(params, fn [name, sku, unit, price, min, max] -> + Craftplan.Inventory.Material + |> Ash.Changeset.for_create(:create, %{ + name: name, + sku: sku, + unit: unit, + price: Decimal.new(price), + minimum_stock: Decimal.new(min), + maximum_stock: Decimal.new(max) + }) + |> Ash.create(authorize?: false) + end) + end +end diff --git a/priv/repo/seeds/20260317232918_add_allergens.exs b/priv/repo/seeds/20260317232918_add_allergens.exs new file mode 100644 index 00000000..d40ff26c --- /dev/null +++ b/priv/repo/seeds/20260317232918_add_allergens.exs @@ -0,0 +1,31 @@ +defmodule Craftplan.Repo.Seeds.AddAllergens do + @moduledoc false + use Craftplan.Seed + + envs([:dev, :prod]) + + def up(_repo, _) do + params = [ + "Gluten", + "Fish", + "Milk", + "Mustard", + "Lupin", + "Crustaceans", + "Peanuts", + "Tree Nuts", + "Sesame", + "Mollusks", + "Eggs", + "Soy", + "Celery", + "Sulphur Dioxide" + ] + + Enum.each(params, fn name -> + Craftplan.Inventory.Allergen + |> Ash.Changeset.for_create(:create, %{name: name}) + |> Ash.create(authorize?: false) + end) + end +end diff --git a/priv/repo/seeds/20260317233646_add_nutritional_facts.exs b/priv/repo/seeds/20260317233646_add_nutritional_facts.exs new file mode 100644 index 00000000..1c35c5c6 --- /dev/null +++ b/priv/repo/seeds/20260317233646_add_nutritional_facts.exs @@ -0,0 +1,31 @@ +defmodule Craftplan.Repo.Seeds.AddNutritionalFacts do + @moduledoc false + use Craftplan.Seed + + envs([:dev, :prod]) + + def up(_repo, _) do + params = [ + "Calories", + "Fat", + "Saturated Fat", + "Carbohydrates", + "Sugar", + "Fiber", + "Protein", + "Salt", + "Sodium", + "Calcium", + "Iron", + "Vitamin A", + "Vitamin C", + "Vitamin D" + ] + + Enum.each(params, fn name -> + Craftplan.Inventory.NutritionalFact + |> Ash.Changeset.for_create(:create, %{name: name}) + |> Ash.create(authorize?: false) + end) + end +end diff --git a/priv/repo/seeds/20260317234014_link_material_allergen.exs b/priv/repo/seeds/20260317234014_link_material_allergen.exs new file mode 100644 index 00000000..0577c1a4 --- /dev/null +++ b/priv/repo/seeds/20260317234014_link_material_allergen.exs @@ -0,0 +1,23 @@ +defmodule Craftplan.Repo.Seeds.LinkMaterialAllergen do + @moduledoc false + use Craftplan.Seed + + envs([:dev, :prod]) + + def up(_repo, _) do + materials = Craftplan.Inventory.list_materials!(authorize?: false) + allergens = Craftplan.Inventory.list_allergens!(authorize?: false) + + Enum.each(1..50, fn _ -> + material = Enum.random(materials) + allergen = Enum.random(allergens) + + Craftplan.Inventory.MaterialAllergen + |> Ash.Changeset.for_create(:create, %{ + material_id: material.id, + allergen_id: allergen.id + }) + |> Ash.create(authorize?: false) + end) + end +end diff --git a/priv/repo/seeds/20260317234034_link_material_nutritional_fact.exs b/priv/repo/seeds/20260317234034_link_material_nutritional_fact.exs new file mode 100644 index 00000000..5e4cac66 --- /dev/null +++ b/priv/repo/seeds/20260317234034_link_material_nutritional_fact.exs @@ -0,0 +1,27 @@ +defmodule Craftplan.Repo.Seeds.LinkMaterialNutritionalFact do + @moduledoc false + use Craftplan.Seed + + envs([:dev, :prod]) + + def up(_repo, _) do + materials = Craftplan.Inventory.list_materials!(authorize?: false) + nutritional_facts = Craftplan.Inventory.list_nutritional_facts!(authorize?: false) + + Enum.each(1..50, fn _ -> + amount = Enum.random(1..200) + material = Enum.random(materials) + nutritional_fact = Enum.random(nutritional_facts) + unit = Enum.random([:gram, :milligram, :kcal]) + + Craftplan.Inventory.MaterialNutritionalFact + |> Ash.Changeset.for_create(:create, %{ + material_id: material, + nutritional_fact_id: nutritional_fact, + amount: Decimal.new(amount), + unit: unit + }) + |> Ash.create(authorize?: false) + end) + end +end diff --git a/priv/repo/seeds/20260317234657_add_initial_stock.exs b/priv/repo/seeds/20260317234657_add_initial_stock.exs new file mode 100644 index 00000000..a723661b --- /dev/null +++ b/priv/repo/seeds/20260317234657_add_initial_stock.exs @@ -0,0 +1,21 @@ +defmodule Craftplan.Repo.Seeds.AddInitialStock do + @moduledoc false + use Craftplan.Seed + + envs([:dev, :prod]) + + def up(_repo, _) do + Enum.each(Craftplan.Inventory.list_materials!(authorize?: false), fn material -> + quantity = Enum.random(1..200) + + Craftplan.Inventory.Movement + |> Ash.Changeset.for_create(:create, %{ + material_id: material.id, + occurred_at: DateTime.utc_now(), + quantity: Decimal.new(quantity), + reason: "Initial stock" + }) + |> Ash.create(authorize?: false) + end) + end +end diff --git a/priv/repo/seeds/20260317235225_seed_lot.exs b/priv/repo/seeds/20260317235225_seed_lot.exs new file mode 100644 index 00000000..2e3a2d9b --- /dev/null +++ b/priv/repo/seeds/20260317235225_seed_lot.exs @@ -0,0 +1,40 @@ +defmodule Craftplan.Repo.Seeds.SeedLot do + @moduledoc false + use Craftplan.Seed + + envs([:dev, :prod]) + + def up(_repo, _) do + materials = Craftplan.Inventory.list_materials!(authorize?: false) + suppliers = Craftplan.Inventory.list_suppliers!(authorize?: false) + + Enum.each(1..50, fn _ -> + quantity = Enum.random(1..200) + material = Enum.random(materials) + supplier = Enum.random(suppliers) + expiry_in_days = Enum.random(1..14) + lot_code = "LOT#{material.name}_#{Date.to_string(Date.utc_today())}_#{Enum.random(1..14)}" + + lot = + Craftplan.Inventory.Lot + |> Ash.Changeset.for_create(:create, %{ + lot_code: lot_code, + material_id: material.id, + supplier_id: supplier && supplier.id, + received_at: DateTime.utc_now(), + expiry_date: Date.add(Date.utc_today(), expiry_in_days) + }) + |> Ash.create(authorize?: false) + + Craftplan.Inventory.Movement + |> Ash.Changeset.for_create(:create, %{ + material_id: material.id, + lot_id: lot.id, + occurred_at: DateTime.utc_now(), + quantity: Decimal.new(quantity), + reason: "Received lot #{lot_code}" + }) + |> Ash.create(authorize?: false) + end) + end +end diff --git a/priv/repo/seeds/20260317235451_seed_product.exs b/priv/repo/seeds/20260317235451_seed_product.exs new file mode 100644 index 00000000..cea308a3 --- /dev/null +++ b/priv/repo/seeds/20260317235451_seed_product.exs @@ -0,0 +1,32 @@ +defmodule Craftplan.Repo.Seeds.SeedProduct do + @moduledoc false + use Craftplan.Seed + + envs([:dev, :prod]) + + def up(_repo, _) do + params = [ + ["Almond Cookies", "COOK-001", "3.99"], + ["Chocolate Cake", "CAKE-001", "15.99"], + ["Artisan Bread", "BREAD-001", "4.99"], + ["Blueberry Muffins", "MUF-001", "2.99"], + ["Butter Croissants", "PAST-001", "2.50"], + ["Gluten-Free Cupcakes", "CUP-001", "3.49"], + ["Rye Loaf Bread", "BREAD-002", "5.49"], + ["Carrot Cake", "CAKE-002", "12.99"], + ["Oatmeal Cookies", "COOK-002", "3.49"], + ["Cheese Danish", "PAST-002", "2.99"] + ] + + Enum.each(params, fn [name, sku, price] -> + Craftplan.Catalog.Product + |> Ash.Changeset.for_create(:create, %{ + name: name, + sku: sku, + status: :active, + price: Decimal.new(price) + }) + |> Ash.create(authorize?: false) + end) + end +end diff --git a/priv/repo/seeds/20260317235630_seed_bom.exs b/priv/repo/seeds/20260317235630_seed_bom.exs new file mode 100644 index 00000000..8438ff40 --- /dev/null +++ b/priv/repo/seeds/20260317235630_seed_bom.exs @@ -0,0 +1,96 @@ +defmodule Craftplan.Repo.Seeds.SeedBom do + @moduledoc false + use Craftplan.Seed + + envs([:dev, :prod]) + + def up(_repo, _) do + products = Craftplan.Catalog.list_products!(authorize?: false) + materials = Craftplan.Inventory.list_materials!(authorize?: false) + + labor_types = [ + "Mix & knead", + "Bake loaves", + "Bulk proof", + "Bake", + "Fill tins", + "Laminate butter", + "Proof", + "Pipe", + "Frost & decorate", + "Bake Layers", + "Cream butter & sugar", + "Fold dry ingredients", + "Prepare filling", + "Frost & finish", + "Bake tests", + "Prep dough" + ] + + labor_defs = + Enum.map(labor_types, fn x -> + %{ + name: x, + duration_minutes: Decimal.new(Enum.random(1..25)), + units_per_run: Decimal.new(Enum.random(1..25)) + } + end) + + component_defs = + Enum.map(materials, fn x -> + %{ + component_type: :material, + material_id: x.id, + quantity: Decimal.new(Enum.random(1..25)) + } + end) + + Enum.each(1..25, fn _ -> + product = Enum.random(products) + opts = [status: :active, name: "#{product.name}_v1"] + + component_defs = Enum.take(component_defs, Enum.random(1..Enum.count(component_defs))) + labor_defs = Enum.take(labor_defs, Enum.random(1..Enum.count(labor_defs))) + + status = Keyword.get(opts, :status, :draft) + + published_at = + case Keyword.get(opts, :published_at) do + nil -> + if status == :active do + DateTime.utc_now() + end + + value -> + value + end + + components = + component_defs + |> Enum.with_index(1) + |> Enum.map(fn {attrs, position} -> + Map.put(attrs, :position, position) + end) + + labor_steps = + labor_defs + |> Enum.with_index(1) + |> Enum.map(fn {attrs, sequence} -> + attrs + |> Map.put(:sequence, sequence) + |> Map.put_new(:units_per_run, Decimal.new("1")) + end) + + Catalog.BOM + |> Ash.Changeset.for_create(:create, %{ + product_id: product.id, + name: Keyword.get(opts, :name, "#{product.name} BOM"), + status: status, + published_at: published_at, + components: components, + labor_steps: labor_steps + }) + |> Ash.create!(authorize?: false) + end) + end +end diff --git a/priv/repo/seeds/20260317235851_seed_supplier.exs b/priv/repo/seeds/20260317235851_seed_supplier.exs new file mode 100644 index 00000000..33ff2514 --- /dev/null +++ b/priv/repo/seeds/20260317235851_seed_supplier.exs @@ -0,0 +1,19 @@ +defmodule Craftplan.Repo.Seeds.SeedSupplier do + @moduledoc false + use Craftplan.Seed + + envs([:dev, :prod]) + + def up(_repo, _) do + params = [{"Fresh Dairy Ltd.", "sales@dairy.test"}, {"Miller & Co.", "hello@miller.test"}] + + Enum.each(params, fn {name, email} -> + Craftplan.Inventory.Supplier + |> Ash.Changeset.for_create(:create, %{ + name: name, + contact_email: email + }) + |> Ash.create(authorize?: false) + end) + end +end diff --git a/priv/repo/seeds/20260318000202_seed_purchase_order.exs b/priv/repo/seeds/20260318000202_seed_purchase_order.exs new file mode 100644 index 00000000..c990c480 --- /dev/null +++ b/priv/repo/seeds/20260318000202_seed_purchase_order.exs @@ -0,0 +1,22 @@ +defmodule Craftplan.Repo.Seeds.SeedPurchaseOrder do + @moduledoc false + use Craftplan.Seed + + envs([:dev, :prod]) + + def up(_repo, _) do + suppliers = Craftplan.Inventory.list_suppliers!(authorize?: false) + + Enum.each(1..25, fn _ -> + status = Enum.random([:draft, :ordered, :received, :cancelled]) + supplier = Enum.random(suppliers) + + Craftplan.Inventory.PurchaseOrder + |> Ash.Changeset.for_create(:create, %{ + supplier_id: supplier.id, + ordered_at: DateTime.utc_now() + }) + |> Ash.create(authorize?: false) + end) + end +end diff --git a/priv/repo/seeds/20260318000530_seed_purchase_order_item.exs b/priv/repo/seeds/20260318000530_seed_purchase_order_item.exs new file mode 100644 index 00000000..afda091b --- /dev/null +++ b/priv/repo/seeds/20260318000530_seed_purchase_order_item.exs @@ -0,0 +1,27 @@ +defmodule Craftplan.Repo.Seeds.SeedPurchaseOrderItem do + @moduledoc false + use Craftplan.Seed + + envs([:dev, :prod]) + + def up(_repo, _) do + materials = Craftplan.Inventory.list_materials!(authorize?: false) + po = Craftplan.Inventory.list_purchase_orders!(authorize?: false) + + Enum.each(1..25, fn _ -> + material = Enum.random(materials) + po = Enum.random(po) + quantity = Enum.random(1..200) + unit_price = Enum.random(1..200) + + Craftplan.Inventory.PurchaseOrderItem + |> Ash.Changeset.for_create(:create, %{ + purchase_order_id: po.id, + material_id: material.id, + quantity: Decimal.new(quantity), + unit_price: Decimal.new(unit_price) + }) + |> Ash.create(authorize?: false) + end) + end +end diff --git a/priv/repo/seeds/20260318000703_seed_customer.exs b/priv/repo/seeds/20260318000703_seed_customer.exs new file mode 100644 index 00000000..12800d63 --- /dev/null +++ b/priv/repo/seeds/20260318000703_seed_customer.exs @@ -0,0 +1,38 @@ +defmodule Craftplan.Repo.Seeds.SeedCustomer do + @moduledoc false + use Craftplan.Seed + + envs([:dev, :prod]) + + def up(_repo, _) do + email = + Faker.Person.first_name() <> + "_" <> Faker.Person.last_name() <> "@" <> Faker.Internet.En.free_email_service() + + params = + for _ <- 1..50 do + {Faker.Person.first_name(), Faker.Person.last_name(), email, "craftplan", + %{ + street: Faker.Address.En.street_address(), + city: Faker.Address.En.city(), + state: Faker.Address.En.state_abbr(), + zip: Faker.Address.En.zip_code(), + country: Faker.Address.En.country() + }} + end + + Enum.each(params, fn {first_name, last_name, email, phone, address_map} -> + Craftplan.CRM.Customer + |> Ash.Changeset.for_create(:create, %{ + type: :individual, + first_name: first_name, + last_name: last_name, + email: email, + phone: phone, + billing_address: address_map, + shipping_address: address_map + }) + |> Ash.create(authorize?: false) + end) + end +end diff --git a/priv/repo/seeds/20260318000818_seed_order.exs b/priv/repo/seeds/20260318000818_seed_order.exs new file mode 100644 index 00000000..3f5d23dd --- /dev/null +++ b/priv/repo/seeds/20260318000818_seed_order.exs @@ -0,0 +1,43 @@ +defmodule Craftplan.Repo.Seeds.SeedOrder do + @moduledoc false + use Craftplan.Seed + + envs([:dev, :prod]) + + def up(_repo, _) do + customers = Craftplan.CRM.list_customers!(authorize?: false) + + Enum.each(1..25, fn _ -> + customer = Enum.random(customers) + delivery_in_days = Enum.random(1..25) + + status = + Enum.random([ + :unconfirmed, + :confirmed, + :in_progress, + :ready, + :delivered, + :completed, + :cancelled + ]) + + payment_status = + Enum.random([ + :pending, + :paid, + :to_be_refunded, + :refunded + ]) + + Craftplan.Orders.Order + |> Ash.Changeset.for_create(:create, %{ + customer_id: customer.id, + delivery_date: DateTime.add(DateTime.utc_now(), delivery_in_days, :day), + status: status, + payment_status: payment_status + }) + |> Ash.create(authorize?: false) + end) + end +end diff --git a/priv/repo/seeds/20260318000911_seed_order_items.exs b/priv/repo/seeds/20260318000911_seed_order_items.exs new file mode 100644 index 00000000..899afe43 --- /dev/null +++ b/priv/repo/seeds/20260318000911_seed_order_items.exs @@ -0,0 +1,28 @@ +defmodule Craftplan.Repo.Seeds.SeedOrderItems do + @moduledoc false + use Craftplan.Seed + + envs([:dev, :prod]) + + def up(_repo, _) do + orders = Craftplan.Orders.list_orders!(authorize?: false) + products = Craftplan.Catalog.list_products!(authorize?: false) + + Enum.each(1..25, fn _ -> + status = Enum.random([:todo, :in_progress, :done]) + order = Enum.random(orders) + product = Enum.random(products) + quantity = Enum.random(1..200) + + Craftplan.Orders.OrderItem + |> Ash.Changeset.for_create(:create, %{ + order_id: order.id, + product_id: product.id, + quantity: Decimal.new(quantity), + unit_price: product.price, + status: status + }) + |> Ash.create(authorize?: false) + end) + end +end diff --git a/priv/static/images/bread.svg b/priv/static/images/logo.svg similarity index 100% rename from priv/static/images/bread.svg rename to priv/static/images/logo.svg diff --git a/rel/overlays/bin/migrate b/rel/overlays/bin/migrate index fe4f8eae..f699f617 100755 --- a/rel/overlays/bin/migrate +++ b/rel/overlays/bin/migrate @@ -1,5 +1,5 @@ #!/bin/sh set -eu -cd -P -- "$(dirname -- "$0")/.." -exec ./bin/craftplan eval Craftplan.Release.migrate +cd -P -- "$(dirname -- "$0")" +exec ./craftplan eval Craftplan.Release.migrate diff --git a/rel/overlays/bin/server b/rel/overlays/bin/server index 63119342..fff4adea 100755 --- a/rel/overlays/bin/server +++ b/rel/overlays/bin/server @@ -1,6 +1,5 @@ #!/bin/sh set -eu -cd -P -- "$(dirname -- "$0")/.." -./bin/migrate -PHX_SERVER=true exec ./bin/craftplan start +cd -P -- "$(dirname -- "$0")" +PHX_SERVER=true exec ./craftplan start From e2c8f426c0658a9c0c04dd2c2d83ac16bfaf23d1 Mon Sep 17 00:00:00 2001 From: mithereal Date: Fri, 20 Mar 2026 11:45:25 -0700 Subject: [PATCH 2/2] fix: split featureset split featureset into smaller chunks --- config/runtime.exs | 9 ++--- lib/craftplan_web/components/layouts.ex | 36 +++++++++++++++++-- .../controllers/page_html/home.html.heex | 2 +- .../live/manage/customer_live/index.ex | 5 --- .../live/manage/order_live/index.ex | 3 -- 5 files changed, 37 insertions(+), 18 deletions(-) diff --git a/config/runtime.exs b/config/runtime.exs index 88f53dd8..ee019930 100644 --- a/config/runtime.exs +++ b/config/runtime.exs @@ -18,8 +18,7 @@ import Config # script that automatically sets the env var above. alias Swoosh.ApiClient.Finch -if System.get_env("PHX_SERVER") || - System.get_env("RAILWAY_PUBLIC_DOMAIN") do +if System.get_env("PHX_SERVER") do config :craftplan, CraftplanWeb.Endpoint, server: true end @@ -45,11 +44,7 @@ if config_env() == :prod do You can generate one by running: openssl rand -base64 48 """ - host = - System.get_env("HOST") || System.get_env("PHX_HOST") || - System.get_env("RAILWAY_PUBLIC_DOMAIN") || - "localhost" - + host = System.get_env("HOST") || System.get_env("PHX_HOST") || "example.com" port = String.to_integer(System.get_env("PORT") || "4000") config :craftplan, Craftplan.Repo, diff --git a/lib/craftplan_web/components/layouts.ex b/lib/craftplan_web/components/layouts.ex index 141e00f9..a33fc4c3 100644 --- a/lib/craftplan_web/components/layouts.ex +++ b/lib/craftplan_web/components/layouts.ex @@ -674,8 +674,40 @@ defmodule CraftplanWeb.Layouts do defp logo_link(assigns) do ~H""" <.link navigate={~p"/"} class="flex items-center gap-2"> - Logo - Craftplan + + + + + + + + + + + Craftplan + """ end diff --git a/lib/craftplan_web/controllers/page_html/home.html.heex b/lib/craftplan_web/controllers/page_html/home.html.heex index 5eeb7135..637b98e1 100644 --- a/lib/craftplan_web/controllers/page_html/home.html.heex +++ b/lib/craftplan_web/controllers/page_html/home.html.heex @@ -1,7 +1,7 @@
- Logo + Craftplan Logo

Craftplan · {@release_version} diff --git a/lib/craftplan_web/live/manage/customer_live/index.ex b/lib/craftplan_web/live/manage/customer_live/index.ex index 0e429a13..9eb470a7 100644 --- a/lib/craftplan_web/live/manage/customer_live/index.ex +++ b/lib/craftplan_web/live/manage/customer_live/index.ex @@ -10,11 +10,6 @@ defmodule CraftplanWeb.CustomerLive.Index do <.header> Customers <:subtitle>Manage your customer records - <:actions> - <.link patch={~p"/manage/customers/new"}> - <.button variant={:primary}>New Customer - - <.table diff --git a/lib/craftplan_web/live/manage/order_live/index.ex b/lib/craftplan_web/live/manage/order_live/index.ex index 8e047b6b..01028847 100644 --- a/lib/craftplan_web/live/manage/order_live/index.ex +++ b/lib/craftplan_web/live/manage/order_live/index.ex @@ -57,9 +57,6 @@ defmodule CraftplanWeb.OrderLive.Index do <:actions> - <.link patch={~p"/manage/orders/new"}> - <.button variant={:primary}>New Order -