ammocpr/lib/ammoprices/catalog.ex
Graham McIntire bbe5bde145
Initial implementation of ammo price tracker
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
2026-03-11 15:58:12 -05:00

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