neiam
/apollos_crib
It's Apollo's Crib
public · Issues · Pulls 1 · Labels · Forks · Compare · Actions failure · Badges · Packages
⭐
Log in to mark this repository.
Packages: each USPS batch builds on the last, so earlier batches' updates stay
8021671 · Gabe Morell · 2026-11-04 21:42
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