# 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