defmodule Ammoprices.PricesTest do use Ammoprices.DataCase, async: true import Ammoprices.Fixtures alias Ammoprices.Prices describe "create_snapshot/2" do test "creates a price snapshot for a product" do product = product_fixture() now = DateTime.truncate(DateTime.utc_now(), :second) attrs = %{ price_cents: 1599, price_per_round_cents: 32, in_stock: true, recorded_at: now } assert {:ok, snapshot} = Prices.create_snapshot(product.id, attrs) assert snapshot.product_id == product.id assert snapshot.price_cents == 1599 assert snapshot.price_per_round_cents == 32 assert snapshot.recorded_at == now end test "returns error with invalid data" do product = product_fixture() assert {:error, changeset} = Prices.create_snapshot(product.id, %{}) assert %{price_cents: ["can't be blank"]} = errors_on(changeset) end end describe "latest_prices_for_caliber/2" do test "returns most recent snapshot per product sorted by price_per_round" do caliber = caliber_fixture() retailer = retailer_fixture() cheap_product = product_fixture(%{caliber: caliber, retailer: retailer, title: "Cheap"}) expensive_product = product_fixture(%{caliber: caliber, retailer: retailer, title: "Expensive"}) now = DateTime.truncate(DateTime.utc_now(), :second) old = DateTime.add(now, -3600, :second) # Old snapshot for cheap product (higher price) snapshot_fixture(%{product: cheap_product, price_per_round_cents: 50, recorded_at: old}) # Latest snapshot for cheap product (lower price) snapshot_fixture(%{product: cheap_product, price_per_round_cents: 25, recorded_at: now}) # Latest snapshot for expensive product snapshot_fixture(%{product: expensive_product, price_per_round_cents: 40, recorded_at: now}) results = Prices.latest_prices_for_caliber(caliber.id) assert length(results) == 2 # Sorted by price_per_round_cents ASC assert hd(results).price_per_round_cents == 25 assert List.last(results).price_per_round_cents == 40 end test "filters by in_stock" do caliber = caliber_fixture() retailer = retailer_fixture() now = DateTime.truncate(DateTime.utc_now(), :second) in_stock_product = product_fixture(%{caliber: caliber, retailer: retailer, in_stock: true}) oos_product = product_fixture(%{caliber: caliber, retailer: retailer, in_stock: false}) snapshot_fixture(%{product: in_stock_product, in_stock: true, recorded_at: now}) snapshot_fixture(%{product: oos_product, in_stock: false, recorded_at: now}) results = Prices.latest_prices_for_caliber(caliber.id, in_stock: true) assert length(results) == 1 assert hd(results).in_stock == true end test "respects limit option" do caliber = caliber_fixture() retailer = retailer_fixture() now = DateTime.truncate(DateTime.utc_now(), :second) for i <- 1..5 do p = product_fixture(%{caliber: caliber, retailer: retailer}) snapshot_fixture(%{product: p, price_per_round_cents: i * 10, recorded_at: now}) end results = Prices.latest_prices_for_caliber(caliber.id, limit: 3) assert length(results) == 3 end end describe "daily_averages_for_caliber/2" do test "returns daily avg/min/max of price_per_round_cents" do caliber = caliber_fixture() retailer = retailer_fixture() product = product_fixture(%{caliber: caliber, retailer: retailer}) today = DateTime.truncate(DateTime.utc_now(), :second) yesterday = DateTime.add(today, -86_400, :second) snapshot_fixture(%{product: product, price_per_round_cents: 30, recorded_at: today}) snapshot_fixture(%{product: product, price_per_round_cents: 40, recorded_at: today}) snapshot_fixture(%{product: product, price_per_round_cents: 20, recorded_at: yesterday}) results = Prices.daily_averages_for_caliber(caliber.id, days: 7) assert length(results) == 2 # Results ordered by date ASC [day1, day2] = results assert day1.min_ppr == 20 assert day1.max_ppr == 20 assert day2.min_ppr == 30 assert day2.max_ppr == 40 assert day2.avg_ppr == 35 end end describe "price_history_for_product/2" do test "returns all snapshots for a product in date range" do product = product_fixture() now = DateTime.truncate(DateTime.utc_now(), :second) for i <- 0..4 do snapshot_fixture(%{ product: product, price_per_round_cents: 30 + i, recorded_at: DateTime.add(now, -i * 86_400, :second) }) end results = Prices.price_history_for_product(product.id, days: 2) # Should include today, yesterday, and day-before (boundary) = 3 of 5 snapshots assert length(results) == 3 end end describe "price_stats_for_caliber/1" do test "returns min, max, avg stats for a caliber" do caliber = caliber_fixture() retailer = retailer_fixture() now = DateTime.truncate(DateTime.utc_now(), :second) product = product_fixture(%{caliber: caliber, retailer: retailer, in_stock: true}) snapshot_fixture(%{product: product, price_per_round_cents: 20, in_stock: true, recorded_at: now}) snapshot_fixture(%{ product: product, price_per_round_cents: 40, in_stock: true, recorded_at: DateTime.add(now, -3600, :second) }) snapshot_fixture(%{ product: product, price_per_round_cents: 30, in_stock: true, recorded_at: DateTime.add(now, -7200, :second) }) stats = Prices.price_stats_for_caliber(caliber.id) assert stats.current_min == 20 assert stats.all_time_low == 20 assert stats.all_time_high == 40 assert is_integer(stats.thirty_day_avg) end test "returns thirty_day_avg based on recent snapshots" do caliber = caliber_fixture() retailer = retailer_fixture() now = DateTime.truncate(DateTime.utc_now(), :second) product = product_fixture(%{caliber: caliber, retailer: retailer, in_stock: true}) # Recent snapshots (within 30 days) snapshot_fixture(%{product: product, price_per_round_cents: 30, in_stock: true, recorded_at: now}) snapshot_fixture(%{ product: product, price_per_round_cents: 40, in_stock: true, recorded_at: DateTime.add(now, -86_400, :second) }) # Old snapshot (outside 30 days) snapshot_fixture(%{ product: product, price_per_round_cents: 100, in_stock: true, recorded_at: DateTime.add(now, -40 * 86_400, :second) }) stats = Prices.price_stats_for_caliber(caliber.id) # 30-day avg should be (30+40)/2 = 35, not include the 100 assert stats.thirty_day_avg == 35 end test "returns nil stats when no snapshots exist" do caliber = caliber_fixture() stats = Prices.price_stats_for_caliber(caliber.id) assert stats.current_min == nil assert stats.all_time_low == nil assert stats.all_time_high == nil assert stats.thirty_day_avg == nil end end describe "cheapest_per_caliber/0" do test "returns min price_per_round for each caliber" do caliber1 = caliber_fixture(%{name: "9mm", slug: "9mm"}) caliber2 = caliber_fixture(%{name: "5.56", slug: "556"}) retailer = retailer_fixture() now = DateTime.truncate(DateTime.utc_now(), :second) p1 = product_fixture(%{caliber: caliber1, retailer: retailer, in_stock: true}) p2 = product_fixture(%{caliber: caliber2, retailer: retailer, in_stock: true}) snapshot_fixture(%{product: p1, price_per_round_cents: 25, in_stock: true, recorded_at: now}) snapshot_fixture(%{product: p1, price_per_round_cents: 30, in_stock: true, recorded_at: now}) snapshot_fixture(%{product: p2, price_per_round_cents: 35, in_stock: true, recorded_at: now}) results = Prices.cheapest_per_caliber() result_map = Map.new(results, fn r -> {r.caliber_id, r.min_ppr} end) assert result_map[caliber1.id] == 25 assert result_map[caliber2.id] == 35 end end end