berrypod/priv/repo/dev_seeds.exs

400 lines
13 KiB
Elixir
Raw Permalink Normal View History

# Dev seed data for testing pagination.
#
# mix run priv/repo/dev_seeds.exs
#
# Creates enough records to exercise pagination in every admin and shop view.
# Safe to run multiple times — skips if data already exists.
alias Berrypod.Repo
alias Berrypod.Products
alias Berrypod.Products.{Product, ProductImage, ProductVariant, ProviderConnection}
alias Berrypod.Orders
alias Berrypod.Orders.{Order, OrderItem}
alias Berrypod.Media.Image
alias Berrypod.Newsletter
alias Berrypod.Redirects
alias Berrypod.Redirects.{Redirect, BrokenUrl}
import Ecto.Query
# ── Helpers ──
defmodule DevSeeds do
@adj ~w[rustic vintage modern classic artisan handmade organic natural premium deluxe]
@noun ~w[mug poster print tote hoodie tee cap sticker notebook candle]
@colour ~w[crimson slate sage coral midnight forest amber ivory charcoal dusty-rose]
@size ~w[XS S M L XL 2XL]
@first_names ~w[Alice Bob Charlie Diana Ethan Fiona George Hannah Ivan Julia
Kevin Laura Mike Nora Oscar Penny Quinn Rosa Sam Tina]
@last_names ~w[Smith Jones Brown Taylor Wilson Davies Evans Thomas Roberts Johnson
Walker White Harris Martin Thompson Garcia Martinez Robinson Clark Lewis]
@domains ~w[gmail.com outlook.com yahoo.co.uk hotmail.com protonmail.com icloud.com]
@campaign_subjects [
"New arrivals just dropped",
"Spring sale — 20% off everything",
"Your exclusive early access",
"Back in stock: fan favourites",
"Free shipping this weekend only",
"Our story so far",
"Last chance: sale ends tonight",
"Meet the maker",
"Gift guide for every budget",
"Something special is coming"
]
def product_title(i) do
adj = Enum.at(@adj, rem(i, length(@adj)))
noun = Enum.at(@noun, rem(div(i, length(@adj)), length(@noun)))
"#{String.capitalize(adj)} #{noun}"
end
def category(i) do
cats = ["Apparel", "Homeware", "Stationery", "Accessories", "Art Prints"]
Enum.at(cats, rem(i, length(cats)))
end
def colour(i), do: Enum.at(@colour, rem(i, length(@colour)))
def size(i), do: Enum.at(@size, rem(i, length(@size)))
def customer_email(i) do
first = Enum.at(@first_names, rem(i, length(@first_names)))
last = Enum.at(@last_names, rem(div(i, length(@first_names)), length(@last_names)))
domain = Enum.at(@domains, rem(i, length(@domains)))
"#{String.downcase(first)}.#{String.downcase(last)}@#{domain}"
end
def subscriber_email(i) do
first = Enum.at(@first_names, rem(i, length(@first_names)))
last = Enum.at(@last_names, rem(div(i + 3, length(@first_names)), length(@last_names)))
domain = Enum.at(@domains, rem(i + 2, length(@domains)))
"#{String.downcase(first)}.#{String.downcase(last)}+news@#{domain}"
end
def campaign_subject(i) do
base = Enum.at(@campaign_subjects, rem(i, length(@campaign_subjects)))
if i >= length(@campaign_subjects), do: "#{base} (#{div(i, length(@campaign_subjects)) + 1})", else: base
end
def random_price, do: Enum.random(999..4999)
def random_cost(price), do: div(price, 2)
end
# ── Guard: skip if data already exists ──
existing_products = Repo.aggregate(Product, :count)
if existing_products >= 60 do
IO.puts("Already have #{existing_products} products — skipping dev seeds.")
else
# ── 1. Provider connection ──
IO.puts("Creating provider connection...")
conn =
case Products.get_provider_connection_by_type("printify") do
nil ->
{:ok, c} =
Products.create_provider_connection(%{
provider_type: "printify",
name: "Dev Printify Shop",
api_key: "dev_test_key_123",
config: %{"shop_id" => "dev_shop"}
})
c
existing ->
existing
end
# ── 2. Products (60 — enough for 3 pages at 25/page in admin, 3 pages at 24/page in shop) ──
IO.puts("Creating 60 products with variants...")
# Tiny placeholder image (1x1 white pixel WebP)
pixel_webp =
<<82, 73, 70, 70, 36, 0, 0, 0, 87, 69, 66, 80, 86, 80, 56, 32,
16, 0, 0, 0, 48, 1, 0, 157, 1, 42, 1, 0, 1, 0, 1, 0, 52, 37,
164, 0, 3, 112, 0, 254, 251, 148, 0, 0>>
for i <- 0..59 do
title = DevSeeds.product_title(i)
slug = Slug.slugify(title) <> "-#{i}"
price = DevSeeds.random_price()
on_sale = rem(i, 7) == 0
{:ok, product} =
Products.create_product(%{
provider_connection_id: conn.id,
provider_product_id: "dev_prod_#{i}",
title: title,
description: "A lovely #{String.downcase(title)} for your collection. Hand-picked and quality-checked.",
slug: slug,
status: "active",
visible: true,
category: DevSeeds.category(i),
cheapest_price: price,
compare_at_price: if(on_sale, do: price + Enum.random(500..1500), else: nil),
on_sale: on_sale,
in_stock: rem(i, 20) != 0,
provider_data: %{
"options" => [
%{"type" => "color", "name" => "Colour", "values" => [
%{"title" => DevSeeds.colour(i), "colors" => ["#666"]},
%{"title" => DevSeeds.colour(i + 1), "colors" => ["#999"]}
]},
%{"type" => "size", "name" => "Size", "values" => [
%{"title" => "S"}, %{"title" => "M"}, %{"title" => "L"}
]}
]
}
})
# Create a product image (using a stored Image record)
{:ok, img} =
%Image{}
|> Image.changeset(%{
image_type: "product",
filename: "#{slug}.webp",
content_type: "image/webp",
file_size: byte_size(pixel_webp),
data: pixel_webp,
source_width: 1,
source_height: 1,
variants_status: "complete",
alt: title
})
|> Repo.insert()
Products.create_product_image(%{
product_id: product.id,
src: "/images/#{img.id}/original",
image_id: img.id,
position: 0,
alt: title
})
# Create 3 variants per product
for size_idx <- 0..2 do
size = DevSeeds.size(size_idx + 1)
colour = DevSeeds.colour(i)
variant_price = price + size_idx * 200
Products.create_product_variant(%{
product_id: product.id,
provider_variant_id: "dev_var_#{i}_#{size_idx}",
title: "#{size} / #{colour}",
sku: "DEV-#{String.upcase(String.slice(slug, 0..5))}-#{size}",
price: variant_price,
compare_at_price: if(on_sale, do: variant_price + Enum.random(500..1500), else: nil),
cost: DevSeeds.random_cost(variant_price),
options: %{"Size" => size, "Colour" => colour},
is_enabled: true,
is_available: rem(i, 20) != 0
})
end
if rem(i + 1, 20) == 0, do: IO.puts(" #{i + 1}/60 products...")
end
IO.puts(" 60/60 products done.")
# ── 3. Extra media images (enough to fill 2+ pages at 36/page = 80 total, minus the 60 product images) ──
IO.puts("Creating 30 extra media images...")
for i <- 0..29 do
%Image{}
|> Image.changeset(%{
image_type: Enum.at(["media", "header", "icon"], rem(i, 3)),
filename: "media-image-#{i}.webp",
content_type: "image/webp",
file_size: byte_size(pixel_webp),
data: pixel_webp,
source_width: 1,
source_height: 1,
variants_status: "complete",
alt: "Media image #{i + 1}",
tags: Enum.at(["banner", "promo", "lifestyle", "texture"], rem(i, 4))
})
|> Repo.insert!()
end
# ── 4. Orders (60 — enough for 3 pages at 25/page) ──
IO.puts("Creating 60 orders...")
statuses = ["paid", "paid", "paid", "pending", "failed", "refunded"]
fulfilment_statuses = ["unfulfilled", "submitted", "processing", "shipped", "delivered"]
for i <- 0..59 do
email = DevSeeds.customer_email(i)
price = DevSeeds.random_price()
qty = Enum.random(1..3)
subtotal = price * qty
payment_status = Enum.at(statuses, rem(i, length(statuses)))
now = NaiveDateTime.utc_now() |> NaiveDateTime.truncate(:second)
# Spread orders over the last 90 days
days_ago = 90 - div(i * 90, 60)
inserted_at = NaiveDateTime.add(now, -days_ago * 86400, :second)
order_attrs = %{
order_number: "SS-260#{String.pad_leading("#{rem(200 + i, 300)}", 3, "0")}01-#{String.pad_leading("#{1000 + i}", 4, "0")}",
subtotal: subtotal,
shipping_cost: if(subtotal > 3000, do: 0, else: 399),
total: subtotal + if(subtotal > 3000, do: 0, else: 399),
currency: "gbp",
customer_email: email,
payment_status: payment_status,
shipping_address: %{
"name" => String.split(email, [".", "@"]) |> Enum.take(2) |> Enum.map(&String.capitalize/1) |> Enum.join(" "),
"line1" => "#{Enum.random(1..200)} #{Enum.at(~w[High Oak Elm Mill Church Park], rem(i, 6))} Street",
"city" => Enum.at(~w[London Manchester Bristol Edinburgh Cardiff Leeds], rem(i, 6)),
"postal_code" => "#{Enum.at(~w[SW1A EC2R BS1 EH1 CF10 LS1], rem(i, 6))} #{Enum.random(1..9)}#{Enum.at(~w[AA AB BA BB], rem(i, 4))}",
"country" => "GB"
},
fulfilment_status:
if(payment_status == "paid",
do: Enum.at(fulfilment_statuses, rem(i, length(fulfilment_statuses))),
else: "unfulfilled"),
inserted_at: inserted_at,
updated_at: inserted_at
}
{:ok, order} =
%Order{}
|> Order.changeset(order_attrs)
|> Repo.insert()
# Create 1-2 line items
for j <- 0..(qty - 1) do
prod_idx = rem(i + j, 60)
%OrderItem{}
|> OrderItem.changeset(%{
order_id: order.id,
variant_id: "dev_var_#{prod_idx}_0",
product_id: "dev_prod_#{prod_idx}",
product_name: DevSeeds.product_title(prod_idx),
variant_title: "M / #{DevSeeds.colour(prod_idx)}",
quantity: 1,
unit_price: price
})
|> Repo.insert!()
end
if rem(i + 1, 20) == 0, do: IO.puts(" #{i + 1}/60 orders...")
end
IO.puts(" 60/60 orders done.")
# ── 5. Newsletter subscribers (60 — enough for 3 pages at 25/page) ──
IO.puts("Creating 60 newsletter subscribers...")
now = DateTime.utc_now() |> DateTime.truncate(:second)
for i <- 0..59 do
email = DevSeeds.subscriber_email(i)
status = Enum.at(["confirmed", "confirmed", "confirmed", "pending", "unsubscribed"], rem(i, 5))
%Newsletter.Subscriber{}
|> Newsletter.Subscriber.changeset(%{
email: email,
status: status,
confirmed_at: if(status == "confirmed", do: now, else: nil),
unsubscribed_at: if(status == "unsubscribed", do: now, else: nil),
consent_text: "Signed up via dev seeds",
source: Enum.at(["website", "checkout", "import"], rem(i, 3))
})
|> Repo.insert!()
end
# ── 6. Newsletter campaigns (30 — enough for 2 pages at 25/page) ──
IO.puts("Creating 30 newsletter campaigns...")
for i <- 0..29 do
status = Enum.at(["draft", "sent", "sent", "sent", "scheduled"], rem(i, 5))
%Newsletter.Campaign{}
|> Newsletter.Campaign.changeset(%{
subject: DevSeeds.campaign_subject(i),
body: """
Hi there!
#{DevSeeds.campaign_subject(i)} — check out what's new in the shop.
Visit us at https://example.com
Unsubscribe: {{unsubscribe_url}}
""",
status: status,
sent_at: if(status == "sent", do: now, else: nil),
sent_count: if(status == "sent", do: Enum.random(20..55), else: 0),
failed_count: if(status == "sent", do: Enum.random(0..3), else: 0),
scheduled_at: if(status == "scheduled", do: DateTime.add(now, 7, :day), else: nil)
})
|> Repo.insert!()
end
# ── 7. Redirects (30 — enough for 2 pages at 25/page) ──
IO.puts("Creating 30 redirects...")
Redirects.create_table()
for i <- 0..29 do
source = Enum.at(["auto_slug_change", "admin", "auto_product_deleted", "analytics_auto_resolved"], rem(i, 4))
old_slug = "old-product-#{i}-#{:rand.uniform(9999)}"
new_slug = "new-product-#{i}"
%Redirect{}
|> Redirect.changeset(%{
from_path: "/products/#{old_slug}",
to_path: "/products/#{new_slug}",
status_code: if(rem(i, 5) == 0, do: 302, else: 301),
source: source,
hit_count: Enum.random(0..150)
})
|> Repo.insert!()
end
# ── 8. Broken URLs (30 — enough for 2 pages at 25/page) ──
IO.puts("Creating 30 broken URLs...")
for i <- 0..29 do
days_ago = Enum.random(1..60)
first_seen = DateTime.add(now, -days_ago * 86400, :second)
%BrokenUrl{}
|> BrokenUrl.changeset(%{
path: "/products/missing-item-#{i}-#{:rand.uniform(9999)}",
prior_analytics_hits: Enum.random(0..500),
recent_404_count: Enum.random(1..50),
first_seen_at: first_seen,
last_seen_at: DateTime.add(first_seen, Enum.random(0..days_ago) * 86400, :second),
status: "pending"
})
|> Repo.insert!()
end
# Warm the redirects cache so they show up immediately
Redirects.warm_cache()
IO.puts("")
IO.puts("Dev seed data created:")
IO.puts(" 60 products (with variants + images)")
IO.puts(" 90 media images (60 product + 30 extra)")
IO.puts(" 60 orders")
IO.puts(" 60 newsletter subscribers")
IO.puts(" 30 newsletter campaigns")
IO.puts(" 30 redirects")
IO.puts(" 30 broken URLs")
IO.puts("")
IO.puts("All views should now show pagination controls.")
end