From 7b36477644da20a26db7d987119ba41b58a046d9 Mon Sep 17 00:00:00 2001 From: Neel Vinoth Date: Thu, 8 Oct 2026 16:16:13 -0400 Subject: [PATCH 1/5] Send welcome SMS to new riders on portal signup MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Previously, new riders received no automatic SMS when granted portal access. The welcome message (stored as @initial_message) existed in Messaging but was only sent lazily — prepended to the first campaign SMS dispatched to a rider via maybe_send_initial_message/2. This meant riders who hadn't yet been assigned a delivery would have no idea which number to expect Brigade messages from. Changes: - Updated @initial_message text: added "about specific deliveries" to the questions line and removed the reference to the old Brigade number - Added Messaging.send_welcome_sms/2, a public function that sends the welcome SMS immediately, saves it as an SmsMessage record, and sets initial_message_sent: true on the rider's flags. Follows the same opts \\ [] arity as new_sms_message/2 to allow callers to pass sent_by: or override the body if needed - Wired send_welcome_sms/2 into MailchimpImporter.create_or_update/1 on the nil (new rider) path, firing immediately after create_rider_with_user succeeds - Added test covering the full path: Mailchimp sync → new rider created → welcome SMS saved to DB with correct recipient and body Why maybe_send_initial_message/2 is retained: The lazy fallback is kept intentionally to cover legacy rider-only records that predate this change. These riders have initial_message_sent: false and opt_in_to_new_number: true but were never sent the welcome SMS because they entered the system before send_welcome_sms existed. For these riders, maybe_send_initial_message will still fire when their first campaign SMS goes out. Once all legacy riders have initial_message_sent: true, the lazy path can be removed. --- lib/bike_brigade/messaging.ex | 19 ++++++++++++++++++- lib/bike_brigade/tasks/mailchimp_importer.ex | 6 +++++- .../importers/mailchimp_importer_test.exs | 14 ++++++++++++++ 3 files changed, 37 insertions(+), 2 deletions(-) diff --git a/lib/bike_brigade/messaging.ex b/lib/bike_brigade/messaging.ex index e1289ac5..bbc0632c 100644 --- a/lib/bike_brigade/messaging.ex +++ b/lib/bike_brigade/messaging.ex @@ -12,7 +12,7 @@ defmodule BikeBrigade.Messaging do alias BikeBrigade.Riders.Rider alias BikeBrigade.SmsService - @initial_message "Hello! This is the Bike Brigade number. The team uses this number to send you information about deliveries you sign up for, and you can text us here if you have any questions. This number is monitored by the Bike Brigade team -- we'll often sign messages with the person sending them.\n\nWe recommend you add this number to your contacts as Bike Brigade (or update your contacts if you've added our old number).\n\nYou can text STOP to this number to opt-out of receiving text messages from us." + @initial_message "Hello! This is the Bike Brigade number. The team uses this number to send you information about deliveries you sign up for, and you can text us here if you have any questions about specific deliveries. This number is monitored by the Bike Brigade team - we’ll often sign messages with the person sending them.\n\nWe recommend you add this number to your contacts as Bike Brigade.\n\nYou can text STOP to this number to opt-out of receiving text messages from us." @doc """ Returns the list of sms_messages. @@ -213,6 +213,23 @@ defmodule BikeBrigade.Messaging do } end + @doc "Send the welcome SMS to a rider and record it in the database" + def send_welcome_sms(%Rider{} = rider, opts \\ []) do + rider = Repo.get!(Rider, rider.id) |> Repo.preload(:location) + message = new_sms_message(rider, Keyword.put_new(opts, :body, @initial_message)) + + with {:ok, %{status: status, sid: sid}} <- + SmsService.send_sms(message, send_callback: true), + {:ok, _} <- + create_sms_message(message, %{ + sent_at: DateTime.utc_now(), + twilio_status: status, + twilio_sid: sid + }) do + Riders.update_rider(rider, %{flags: %{initial_message_sent: true}}) + end + end + @doc """ Appends the dispatcher's signature to a message body if the user has signatures enabled. Returns the original body unchanged if no sent_by_user_id, user has setting disabled, or user not found. diff --git a/lib/bike_brigade/tasks/mailchimp_importer.ex b/lib/bike_brigade/tasks/mailchimp_importer.ex index 6b491a87..3726c5fb 100644 --- a/lib/bike_brigade/tasks/mailchimp_importer.ex +++ b/lib/bike_brigade/tasks/mailchimp_importer.ex @@ -5,6 +5,7 @@ defmodule BikeBrigade.Tasks.MailchimpImporter do alias BikeBrigade.Locations.Location alias BikeBrigade.Tasks.Importer alias BikeBrigade.Riders + alias BikeBrigade.Messaging alias BikeBrigade.Messaging.Slack alias BikeBrigade.MailchimpApi @@ -84,7 +85,10 @@ defmodule BikeBrigade.Tasks.MailchimpImporter do Riders.update_rider(rider, rider_attrs) nil -> - Riders.create_rider_with_user(rider_attrs) + with {:ok, rider} <- Riders.create_rider_with_user(rider_attrs) do + Messaging.send_welcome_sms(rider) + {:ok, rider} + end end end diff --git a/test/bike_brigade/importers/mailchimp_importer_test.exs b/test/bike_brigade/importers/mailchimp_importer_test.exs index 43a0701c..b0218cf7 100644 --- a/test/bike_brigade/importers/mailchimp_importer_test.exs +++ b/test/bike_brigade/importers/mailchimp_importer_test.exs @@ -7,6 +7,7 @@ defmodule BikeBrigade.Tasks.MailchimpImporterTest do alias BikeBrigade.SlackApi.FakeSlack alias BikeBrigade.Riders + alias BikeBrigade.Messaging @list_id "LIST_ID" @@ -104,6 +105,19 @@ defmodule BikeBrigade.Tasks.MailchimpImporterTest do assert [%{name: "invalid_location"}] = Repo.preload(r, :tags).tags end + test "sends a welcome SMS when a new rider is created" do + FakeMailchimp.add_members(@list_id, [@valid_attrs]) + + assert {:ok, _} = MailchimpImporter.sync_riders() + + rider = Riders.get_rider_by_email("dispatcher@example.com") + messages = Messaging.list_sms_messages() + + welcome = Enum.find(messages, &(&1.to == rider.phone && !&1.incoming)) + assert welcome != nil + assert welcome.body =~ "Hello! This is the Bike Brigade number" + end + test "updates riders" do FakeMailchimp.add_members(@list_id, [@valid_attrs]) From 2ddc5c2d279e45c5cbb5604b15ee6ba2c108c8bf Mon Sep 17 00:00:00 2001 From: Neel Vinoth Date: Thu, 8 Oct 2026 23:16:09 -0400 Subject: [PATCH 2/5] Enhance create_or_update/1 with clause to chain welcome SMS result --- lib/bike_brigade/tasks/mailchimp_importer.ex | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/bike_brigade/tasks/mailchimp_importer.ex b/lib/bike_brigade/tasks/mailchimp_importer.ex index 3726c5fb..a9d4573e 100644 --- a/lib/bike_brigade/tasks/mailchimp_importer.ex +++ b/lib/bike_brigade/tasks/mailchimp_importer.ex @@ -85,8 +85,8 @@ defmodule BikeBrigade.Tasks.MailchimpImporter do Riders.update_rider(rider, rider_attrs) nil -> - with {:ok, rider} <- Riders.create_rider_with_user(rider_attrs) do - Messaging.send_welcome_sms(rider) + with {:ok, rider} <- Riders.create_rider_with_user(rider_attrs), + {:ok, rider} <- Messaging.send_welcome_sms(rider) do {:ok, rider} end end From c7ed947e241314c21b3bf14a254fd813dec2714b Mon Sep 17 00:00:00 2001 From: Neel Vinoth Date: Sun, 18 Oct 2026 16:34:29 -0400 Subject: [PATCH 3/5] Add /new-rider-guide redirect and update welcome SMS with onboarding link - Add runtime-configurable redirect at /new-rider-guide pointing to the new rider onboarding Google Doc - Update welcome SMS to include the guide URL and info@bikebrigade.ca for general questions --- config/config.exs | 5 ++++- lib/bike_brigade/messaging.ex | 2 +- .../controllers/redirect_controller.ex | 13 +++++++++++++ lib/bike_brigade_web/router.ex | 6 ++++++ 4 files changed, 24 insertions(+), 2 deletions(-) create mode 100644 lib/bike_brigade_web/controllers/redirect_controller.ex diff --git a/config/config.exs b/config/config.exs index 9de97d91..00c6be05 100644 --- a/config/config.exs +++ b/config/config.exs @@ -34,7 +34,10 @@ config :reverse_proxy_plug, :http_client, ReverseProxyPlug.HTTPClient.Adapters.H # Analytics reverse proxy config :bike_brigade, BikeBrigadeWeb.Router, - analytics_upstream: "http://bike-brigade-analytics.internal:8080/analytics/" + analytics_upstream: "http://bike-brigade-analytics.internal:8080/analytics/", + # Destination for dispatch.bikebrigade.ca/new-rider-guide — linked in the welcome SMS sent to new riders. + new_rider_guide_url: + "https://docs.google.com/document/d/18fb6YgjMktL1MZ89RhC6aDCt1TZXTGyMWJoSIGicnms/edit?tab=t.0" # Esbuild config :esbuild, diff --git a/lib/bike_brigade/messaging.ex b/lib/bike_brigade/messaging.ex index bbc0632c..55a05dfa 100644 --- a/lib/bike_brigade/messaging.ex +++ b/lib/bike_brigade/messaging.ex @@ -12,7 +12,7 @@ defmodule BikeBrigade.Messaging do alias BikeBrigade.Riders.Rider alias BikeBrigade.SmsService - @initial_message "Hello! This is the Bike Brigade number. The team uses this number to send you information about deliveries you sign up for, and you can text us here if you have any questions about specific deliveries. This number is monitored by the Bike Brigade team - we’ll often sign messages with the person sending them.\n\nWe recommend you add this number to your contacts as Bike Brigade.\n\nYou can text STOP to this number to opt-out of receiving text messages from us." + @initial_message "Hello! This is the Bike Brigade number. The team uses this number to send you information about deliveries you sign up for, and you can text us here if you have any questions about specific deliveries. This number is monitored by the Bike Brigade team - we’ll often sign messages with the person sending them.\n\nWe recommend you add this number to your contacts as Bike Brigade.\n\nYou can text STOP to this number to opt-out of receiving text messages from us.\n\nFor more info about signing up for your first delivery, see our new rider onboarding guide https://dispatch.bikebrigade.ca/new-rider-guide. For general questions, email info@bikebrigade.ca." @doc """ Returns the list of sms_messages. diff --git a/lib/bike_brigade_web/controllers/redirect_controller.ex b/lib/bike_brigade_web/controllers/redirect_controller.ex new file mode 100644 index 00000000..6b454fdd --- /dev/null +++ b/lib/bike_brigade_web/controllers/redirect_controller.ex @@ -0,0 +1,13 @@ +defmodule BikeBrigadeWeb.RedirectController do + use BikeBrigadeWeb, :controller + + import BikeBrigade.Utils, only: [get_env: 3] + + def new_rider_guide(conn, _params) do + url = + get_env(BikeBrigadeWeb.Router, :new_rider_guide_url, nil) || + raise "NEW_RIDER_GUIDE_URL is not configured" + + redirect(conn, external: url) + end +end diff --git a/lib/bike_brigade_web/router.ex b/lib/bike_brigade_web/router.ex index aa288e6a..569846fa 100644 --- a/lib/bike_brigade_web/router.ex +++ b/lib/bike_brigade_web/router.ex @@ -87,6 +87,12 @@ defmodule BikeBrigadeWeb.Router do delete "/login", AuthenticationController, :cancel end + scope "/", BikeBrigadeWeb do + pipe_through [:browser] + + get "/new-rider-guide", RedirectController, :new_rider_guide + end + # this scope is for authenticated users that aren't dispatchers (ie: riders). scope "/", BikeBrigadeWeb do pipe_through [:browser, :require_authenticated_user, :set_honeybadger_context] From eecc74040ffcd77f38049174e9718bcd0bf93359 Mon Sep 17 00:00:00 2001 From: Neel Vinoth Date: Sun, 18 Oct 2026 16:41:06 -0400 Subject: [PATCH 4/5] Add comment to clarify welcome SMS sending logic for new riders --- lib/bike_brigade/messaging.ex | 3 +++ 1 file changed, 3 insertions(+) diff --git a/lib/bike_brigade/messaging.ex b/lib/bike_brigade/messaging.ex index 55a05dfa..024e8c36 100644 --- a/lib/bike_brigade/messaging.ex +++ b/lib/bike_brigade/messaging.ex @@ -329,6 +329,9 @@ defmodule BikeBrigade.Messaging do defp maybe_send_initial_message(%Ecto.Multi{} = multi, nil), do: multi + # Sends the welcome SMS for riders who opted in to the new number via the dispatcher flow. + # Riders signing up through the portal receive the welcome SMS via Riders.create_or_update/1 + # and will not reach this clause (initial_message_sent is already true by then). defp maybe_send_initial_message(%Ecto.Multi{} = multi, %Rider{} = rider) do case rider.flags do %Rider.Flags{opt_in_to_new_number: true, initial_message_sent: false} -> From d1360bba44e81326bc5922e305bee71dafdeb5d6 Mon Sep 17 00:00:00 2001 From: Neel Vinoth Date: Tue, 20 Oct 2026 23:43:03 -0400 Subject: [PATCH 5/5] Refactor MailchimpImporterTest to use @member for consistency in attribute handling --- .../importers/mailchimp_importer_test.exs | 32 +++++++++---------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/test/bike_brigade/importers/mailchimp_importer_test.exs b/test/bike_brigade/importers/mailchimp_importer_test.exs index b0218cf7..cd81be29 100644 --- a/test/bike_brigade/importers/mailchimp_importer_test.exs +++ b/test/bike_brigade/importers/mailchimp_importer_test.exs @@ -11,7 +11,7 @@ defmodule BikeBrigade.Tasks.MailchimpImporterTest do @list_id "LIST_ID" - @valid_attrs %{ + @member %{ id: "123", email_address: "dispatcher@example.com", status: "subscribed", @@ -26,18 +26,18 @@ defmodule BikeBrigade.Tasks.MailchimpImporterTest do describe "parse_mailchimp_attrs/1" do test "is successful if fields from Mailchimp are valid" do - {:ok, expected_fields} = MailchimpImporter.parse_mailchimp_attrs(@valid_attrs) + {:ok, expected_fields} = MailchimpImporter.parse_mailchimp_attrs(@member) address = expected_fields[:location][:address] assert address == "1508 Davenport Rd" end test "returns an error if the phone number is not a valid Canadian number" do - invalid_attrs = put_in(@valid_attrs, [:merge_fields, :PHONEYUI_], "oops") + invalid_attrs = put_in(@member, [:merge_fields, :PHONEYUI_], "oops") assert {:error, _} = MailchimpImporter.parse_mailchimp_attrs(invalid_attrs) end test "returns an error if address fails geocoding" do - invalid_attrs = put_in(@valid_attrs, [:merge_fields, :TEXTYUI_3], "oops") + invalid_attrs = put_in(@member, [:merge_fields, :TEXTYUI_3], "oops") assert {:error, {:update_location, _}} = MailchimpImporter.parse_mailchimp_attrs(invalid_attrs) @@ -62,36 +62,36 @@ defmodule BikeBrigade.Tasks.MailchimpImporterTest do end test "is successful when all required attributes are valid" do - assert Riders.get_rider_by_email("dispatcher@example.com") == nil - FakeMailchimp.add_members(@list_id, [@valid_attrs]) + assert Riders.get_rider_by_email(@member[:email_address]) == nil + FakeMailchimp.add_members(@list_id, [@member]) assert {:ok, _} = MailchimpImporter.sync_riders() - r = Riders.get_rider_by_email("dispatcher@example.com") + r = Riders.get_rider_by_email(@member[:email_address]) assert r.name == "Morty" assert r.location.address == "1508 Davenport Rd" end test "handles case where phone is invalid" do - invalid_attrs = put_in(@valid_attrs, [:merge_fields, :PHONEYUI_], "oops") + invalid_attrs = put_in(@member, [:merge_fields, :PHONEYUI_], "oops") FakeMailchimp.add_members(@list_id, [invalid_attrs]) assert {:ok, _} = MailchimpImporter.sync_riders() - assert Riders.get_rider_by_email("dispatcher@example.com") == nil + assert Riders.get_rider_by_email(@member[:email_address]) == nil call = FakeSlack.get_last_call() assert call[:body] =~ "An error ocurred when importing dispatcher@example.com" end test "handles case where address is invalid" do - invalid_attrs = put_in(@valid_attrs, [:merge_fields, :TEXTYUI_3], "oops") + invalid_attrs = put_in(@member, [:merge_fields, :TEXTYUI_3], "oops") FakeMailchimp.add_members(@list_id, [invalid_attrs]) assert {:ok, _} = MailchimpImporter.sync_riders() - r = Riders.get_rider_by_email("dispatcher@example.com") + r = Riders.get_rider_by_email(@member[:email_address]) assert r.name == "Morty" # The rider has a default location @@ -106,11 +106,11 @@ defmodule BikeBrigade.Tasks.MailchimpImporterTest do end test "sends a welcome SMS when a new rider is created" do - FakeMailchimp.add_members(@list_id, [@valid_attrs]) + FakeMailchimp.add_members(@list_id, [@member]) assert {:ok, _} = MailchimpImporter.sync_riders() - rider = Riders.get_rider_by_email("dispatcher@example.com") + rider = Riders.get_rider_by_email(@member[:email_address]) messages = Messaging.list_sms_messages() welcome = Enum.find(messages, &(&1.to == rider.phone && !&1.incoming)) @@ -119,18 +119,18 @@ defmodule BikeBrigade.Tasks.MailchimpImporterTest do end test "updates riders" do - FakeMailchimp.add_members(@list_id, [@valid_attrs]) + FakeMailchimp.add_members(@list_id, [@member]) assert {:ok, _} = MailchimpImporter.sync_riders() FakeMailchimp.clear_members(@list_id) FakeMailchimp.add_members(@list_id, [ - put_in(@valid_attrs, [:merge_fields, :FNAME], "Mortimus") + put_in(@member, [:merge_fields, :FNAME], "Mortimus") ]) assert {:ok, _} = MailchimpImporter.sync_riders() - r = Riders.get_rider_by_email("dispatcher@example.com") + r = Riders.get_rider_by_email(@member[:email_address]) assert r.name == "Mortimus" end end