- Add subsonic boolean field to products with composite filter indexes - Add TextDetector utility for detecting subsonic/casing from product titles - Wire TextDetector into SgAmmo (casing + subsonic) and LuckyGunner (subsonic + fallback casing) - Extend latest_prices_for_caliber with casing, grain_weight, and subsonic filter options - Add distinct_grain_weights_for_caliber and distinct_casings_for_caliber queries - Add filter UI with toggleable casing, grain weight, and subsonic buttons - Add grain weight column and subsonic badge to product table - Refresh filter options on PubSub price updates for real-time UI - Implement Target Sports USA scraper with 18 caliber mappings - Display all prices in dollar format ($0.38 instead of 38¢)
115 lines
2.3 KiB
Elixir
115 lines
2.3 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,
|
|
:subsonic,
|
|
: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
|
|
|
|
def distinct_grain_weights_for_caliber(caliber_id) do
|
|
Product
|
|
|> where([p], p.caliber_id == ^caliber_id)
|
|
|> where([p], not is_nil(p.grain_weight))
|
|
|> distinct(true)
|
|
|> select([p], p.grain_weight)
|
|
|> order_by([p], asc: p.grain_weight)
|
|
|> Repo.all()
|
|
end
|
|
|
|
def distinct_casings_for_caliber(caliber_id) do
|
|
Product
|
|
|> where([p], p.caliber_id == ^caliber_id)
|
|
|> where([p], not is_nil(p.casing))
|
|
|> distinct(true)
|
|
|> select([p], p.casing)
|
|
|> order_by([p], asc: p.casing)
|
|
|> Repo.all()
|
|
end
|
|
end
|