Packages: each USPS batch builds on the last, so earlier batches' updates stay

8021671 · Gabe Morell · 2026-11-04 21:42

8 files +115 -2
Message
{commit_body(@commit)}

Files changed

modified apps/room_packages/lib/room_packages/package_worker.ex
+7 −2
@@ -57,13 +57,17 @@ defmodule RoomPackages.Worker do
57 57 source.meta.tracking
58 58 |> Enum.filter(fn t -> t.type == :usps and not delivered?(t) end)
59 59
60 + # Each batch starts from the source the last one left behind:
61 + # update_source_meta_tracking/3 rewrites the whole list from the struct
62 + # it is handed, so a batch working from the pre-poll copy would put
63 + # every earlier batch's entries back the way they were.
60 64 pending
61 65 |> Enum.chunk_every(RoomPackages.USPS.max_batch())
62 − |> Enum.each(&refresh_chunk(source, &1))
66 + |> Enum.reduce(source, &refresh_chunk/2)
63 67 end
64 68 end
65 69
66 − defp refresh_chunk(source, chunk) do
70 + defp refresh_chunk(chunk, source) do
67 71 numbers = Enum.map(chunk, & &1.number)
68 72
69 73 case RoomPackages.USPS.track_many(source, numbers) do
@@ -74,6 +78,7 @@ defmodule RoomPackages.Worker do
74 78
75 79 {:error, reason} ->
76 80 Logger.debug("USPS batch of #{length(numbers)}: #{inspect(reason)}")
81 + source
77 82 end
78 83 end
79 84
modified apps/room_packages/mix.exs
+3 −0
@@ -28,6 +28,9 @@ defmodule RoomPackages.MixProject do
28 28 {:httpoison, "~> 3.0", override: true},
29 29 {:jason, "~> 1.4"},
30 30 {:plug_crypto, "~> 2.0"},
31 + # The worker reads and writes sources through RoomSanctum, which the
32 + # umbrella starts in production; its tests need the Repo here too.
33 + {:room_sanctum, in_umbrella: true, only: :test}
31 34 # {:dep_from_hexpm, "~> 0.3.0"},
32 35 # {:dep_from_git, git: "https://github.com/elixir-lang/my_dep.git", tag: "0.1.0"},
33 36 # {:sibling_app_in_umbrella, in_umbrella: true}
added apps/room_packages/test/package_worker_test.exs
+105 −0
@@ -0,0 +1,105 @@
1 +defmodule RoomPackages.WorkerTest do
2 + @moduledoc """
3 + The USPS poll, against a stand-in for the tracking endpoint.
4 +
5 + More pending numbers than one request can carry go out in batches, and each
6 + batch has to build on what the one before it wrote:
7 + `update_source_meta_tracking/3` rewrites the whole tracking list from the
8 + struct it is handed.
9 + """
10 + use ExUnit.Case, async: false
11 +
12 + alias RoomSanctum.{Accounts, Configuration, Repo}
13 +
14 + defmodule FakeUSPS do
15 + @moduledoc false
16 + # Answers every number it is asked about as in transit.
17 + import Plug.Conn
18 +
19 + def init(opts), do: opts
20 +
21 + def call(conn, _opts) do
22 + {:ok, body, conn} = read_body(conn)
23 +
24 + answers =
25 + body
26 + |> Jason.decode!()
27 + |> Enum.map(&%{"trackingNumber" => &1["trackingNumber"], "status" => "In Transit"})
28 +
29 + conn
30 + |> put_resp_content_type("application/json")
31 + |> send_resp(200, Jason.encode!(answers))
32 + end
33 + end
34 +
35 + setup do
36 + :ok = Ecto.Adapters.SQL.Sandbox.checkout(Repo)
37 +
38 + start_supervised!({Plug.Cowboy, scheme: :http, plug: FakeUSPS, options: [port: 0]})
39 + port = :ranch.get_port(FakeUSPS.HTTP)
40 +
41 + previous = Application.get_env(:room_packages, :usps_tracking_base)
42 + Application.put_env(:room_packages, :usps_tracking_base, "http://127.0.0.1:#{port}")
43 +
44 + on_exit(fn ->
45 + case previous do
46 + nil -> Application.delete_env(:room_packages, :usps_tracking_base)
47 + base -> Application.put_env(:room_packages, :usps_tracking_base, base)
48 + end
49 + end)
50 +
51 + {:ok, user} =
52 + Accounts.register_user(%{
53 + email: "parcels#{System.unique_integer([:positive])}@example.com",
54 + password: "hello world!hello world!"
55 + })
56 +
57 + %{user: user}
58 + end
59 +
60 + defp source_tracking(user, count) do
61 + expiry = DateTime.utc_now() |> DateTime.add(3600) |> DateTime.to_iso8601()
62 +
63 + {:ok, source} =
64 + Configuration.create_source(%{
65 + name: "Parcels",
66 + notes: "",
67 + type: :packages,
68 + enabled: true,
69 + user_id: user.id,
70 + config: %{
71 + "__type__" => "packages",
72 + "apikey_usps_id" => "id",
73 + "apikey_usps_secret" => "secret",
74 + # A token that has not expired, so no mint is attempted.
75 + "token_usps" => "token",
76 + "token_usps_expiry" => expiry
77 + },
78 + meta: %{
79 + tracking:
80 + for n <- 1..count do
81 + %{number: "9400#{n}", type: :usps, entries: []}
82 + end
83 + }
84 + })
85 +
86 + source
87 + end
88 +
89 + test "every batch's updates survive when the numbers need more than one request", %{
90 + user: user
91 + } do
92 + count = RoomPackages.USPS.max_batch() + 1
93 + source = source_tracking(user, count)
94 +
95 + {:noreply, state} = RoomPackages.Worker.handle_cast(:refresh, %{id: source.id, data: []})
96 +
97 + tracking = Configuration.get_source!(source.id).meta.tracking
98 + assert length(tracking) == count
99 +
100 + without = for t <- tracking, t.entries in [nil, []], do: t.number
101 + assert without == []
102 +
103 + assert Enum.all?(state.data, &(length(&1.entries) == 1))
104 + end
105 +end

Parents: b0284cc