Phoenix 1.8 app that scrapes ammunition retailers (Lucky Gunner, SGAmmo) for price data and displays historical price trends. - Data model: retailers, calibers, products, price snapshots - Scraper infrastructure with Req, Floki, realistic browser headers - Oban-scheduled scrape jobs (every 4h with randomized delays) - LiveView pages: homepage with category cards, caliber detail with price table, Chart.js price history, and price stats banner - 18 seeded calibers across handgun/rifle/rimfire/shotgun categories - 77 tests
84 lines
1.7 KiB
Elixir
84 lines
1.7 KiB
Elixir
defmodule Ammoprices.Catalog do
|
|
@moduledoc false
|
|
import Ecto.Query
|
|
|
|
alias Ammoprices.Catalog.Caliber
|
|
alias Ammoprices.Catalog.Product
|
|
alias Ammoprices.Catalog.Retailer
|
|
alias Ammoprices.Repo
|
|
|
|
# Retailers
|
|
|
|
def list_retailers do
|
|
Repo.all(Retailer)
|
|
end
|
|
|
|
def get_retailer!(id) do
|
|
Repo.get!(Retailer, id)
|
|
end
|
|
|
|
def get_retailer_by_slug!(slug) do
|
|
Repo.get_by!(Retailer, slug: slug)
|
|
end
|
|
|
|
def create_retailer(attrs) do
|
|
%Retailer{}
|
|
|> Retailer.changeset(attrs)
|
|
|> Repo.insert()
|
|
end
|
|
|
|
def update_retailer(%Retailer{} = retailer, attrs) do
|
|
retailer
|
|
|> Retailer.changeset(attrs)
|
|
|> Repo.update()
|
|
end
|
|
|
|
# Calibers
|
|
|
|
def list_calibers do
|
|
Repo.all(Caliber)
|
|
end
|
|
|
|
def list_calibers_by_category(category) do
|
|
Caliber
|
|
|> where([c], c.category == ^category)
|
|
|> Repo.all()
|
|
end
|
|
|
|
def get_caliber!(id) do
|
|
Repo.get!(Caliber, id)
|
|
end
|
|
|
|
def get_caliber_by_slug!(slug) do
|
|
Repo.get_by!(Caliber, slug: slug)
|
|
end
|
|
|
|
def create_caliber(attrs) do
|
|
%Caliber{}
|
|
|> Caliber.changeset(attrs)
|
|
|> Repo.insert()
|
|
end
|
|
|
|
# Products
|
|
|
|
def upsert_product(retailer_id, caliber_id, attrs) do
|
|
%Product{retailer_id: retailer_id, caliber_id: caliber_id}
|
|
|> Product.changeset(attrs)
|
|
|> Repo.insert(
|
|
on_conflict:
|
|
{:replace,
|
|
[:title, :brand, :grain_weight, :round_count, :casing, :condition, :in_stock, :last_seen_at, :updated_at]},
|
|
conflict_target: [:retailer_id, :url],
|
|
returning: true
|
|
)
|
|
end
|
|
|
|
def list_products_for_caliber(caliber_id, opts \\ []) do
|
|
limit = Keyword.get(opts, :limit, 100)
|
|
|
|
Product
|
|
|> where([p], p.caliber_id == ^caliber_id)
|
|
|> limit(^limit)
|
|
|> Repo.all()
|
|
end
|
|
end
|