From 1588c38b6449a922c2052b44b084626ccdd3c1d3 Mon Sep 17 00:00:00 2001 From: Neel Vinoth Date: Sat, 28 Mar 2026 23:01:29 -0400 Subject: [PATCH 1/2] Add list_campaigns_ended_between/2 to query campaigns by end time https://github.com/bikebrigade/dispatch/issues/476 This function enables finding campaigns whose delivery window ended within a specified time range. It's designed to support a scheduled job that runs every 15 minutes to process recently ended campaigns. The function: - Accepts from_datetime and to_datetime as NaiveDateTime - Uses inclusive bounds (>= and <=) for the time window - Returns a list of Campaign structs without preloads Test coverage includes: - Finding campaigns within the specified window - Returning empty list when no campaigns match --- lib/bike_brigade/delivery.ex | 18 ++++++++++++++++++ test/bike_brigade/delivery_test.exs | 27 +++++++++++++++++++++++++++ 2 files changed, 45 insertions(+) diff --git a/lib/bike_brigade/delivery.ex b/lib/bike_brigade/delivery.ex index 0e921ded..1225320d 100644 --- a/lib/bike_brigade/delivery.ex +++ b/lib/bike_brigade/delivery.ex @@ -311,6 +311,24 @@ defmodule BikeBrigade.Delivery do |> Repo.preload([:program, :tasks]) end + @doc """ + Returns campaigns whose delivery window ended within the given time range. + + ## Parameters + - `from_datetime` - Start of the time window (inclusive) + - `to_datetime` - End of the time window (inclusive) + """ + def list_campaigns_ended_between(from_utc_datetime, to_utc_datetime) do + query = + from c in Campaign, + where: + c.delivery_end >= ^from_utc_datetime and + c.delivery_end <= ^to_utc_datetime, + select: c + + Repo.all(query) + end + @doc """ Fetches how many open vs filled tasks there are (optionally, by week) and groups them by campaign ID. diff --git a/test/bike_brigade/delivery_test.exs b/test/bike_brigade/delivery_test.exs index 33d4d05a..8a81415d 100644 --- a/test/bike_brigade/delivery_test.exs +++ b/test/bike_brigade/delivery_test.exs @@ -345,5 +345,32 @@ defmodule BikeBrigade.DeliveryTest do end end + describe "list_campaigns_ended_between/2" do + setup do + campaign = + fixture(:campaign, %{ + delivery_start: NaiveDateTime.utc_now() |> NaiveDateTime.add(-7, :hour), + delivery_end: NaiveDateTime.utc_now() |> NaiveDateTime.add(-1, :hour) + }) + + %{campaign: campaign} + end + + test "returns a campaign available in the given window", %{campaign: campaign} do + from_datetime = NaiveDateTime.utc_now() |> NaiveDateTime.add(-75, :minute) + to_datetime = NaiveDateTime.utc_now() |> NaiveDateTime.add(-60, :minute) + + [ended_campaign] = Delivery.list_campaigns_ended_between(from_datetime, to_datetime) + assert campaign.id == ended_campaign.id + end + + test "returns no campaign in the given window" do + from_datetime = NaiveDateTime.utc_now() |> NaiveDateTime.add(-45, :minute) + to_datetime = NaiveDateTime.utc_now() |> NaiveDateTime.add(-30, :minute) + + assert [] == Delivery.list_campaigns_ended_between(from_datetime, to_datetime) + end + end + def item_name(%Task{task_items: [%{item: %{name: item_name}}]}), do: item_name end From 0a0c397821f3f969bd34e93c00d47dee8872a191 Mon Sep 17 00:00:00 2001 From: Neel Vinoth Date: Mon, 30 Mar 2026 22:53:20 -0400 Subject: [PATCH 2/2] Fix race condition in list_campaigns_ended_between tests Use a consistent timestamp across setup and tests instead of calling NaiveDateTime.utc_now() multiple times --- test/bike_brigade/delivery_test.exs | 25 ++++++++++++++++--------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/test/bike_brigade/delivery_test.exs b/test/bike_brigade/delivery_test.exs index 8a81415d..0fe218c4 100644 --- a/test/bike_brigade/delivery_test.exs +++ b/test/bike_brigade/delivery_test.exs @@ -347,30 +347,37 @@ defmodule BikeBrigade.DeliveryTest do describe "list_campaigns_ended_between/2" do setup do + now = get_utc_now() + campaign = fixture(:campaign, %{ - delivery_start: NaiveDateTime.utc_now() |> NaiveDateTime.add(-7, :hour), - delivery_end: NaiveDateTime.utc_now() |> NaiveDateTime.add(-1, :hour) + delivery_start: NaiveDateTime.add(now, -7, :hour), + delivery_end: NaiveDateTime.add(now, -1, :hour) }) - %{campaign: campaign} + %{campaign: campaign, now: now} end - test "returns a campaign available in the given window", %{campaign: campaign} do - from_datetime = NaiveDateTime.utc_now() |> NaiveDateTime.add(-75, :minute) - to_datetime = NaiveDateTime.utc_now() |> NaiveDateTime.add(-60, :minute) + test "returns a campaign available in the given window", %{ + campaign: campaign, + now: now + } do + from_datetime = NaiveDateTime.add(now, -75, :minute) + to_datetime = NaiveDateTime.add(now, -60, :minute) [ended_campaign] = Delivery.list_campaigns_ended_between(from_datetime, to_datetime) assert campaign.id == ended_campaign.id end - test "returns no campaign in the given window" do - from_datetime = NaiveDateTime.utc_now() |> NaiveDateTime.add(-45, :minute) - to_datetime = NaiveDateTime.utc_now() |> NaiveDateTime.add(-30, :minute) + test "returns no campaign in the given window", %{now: now} do + from_datetime = NaiveDateTime.add(now, -45, :minute) + to_datetime = NaiveDateTime.add(now, -30, :minute) assert [] == Delivery.list_campaigns_ended_between(from_datetime, to_datetime) end end def item_name(%Task{task_items: [%{item: %{name: item_name}}]}), do: item_name + + defp get_utc_now(), do: NaiveDateTime.utc_now() end