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 e1289ac5..024e8c36 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.\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. @@ -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. @@ -312,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} -> diff --git a/lib/bike_brigade/tasks/mailchimp_importer.ex b/lib/bike_brigade/tasks/mailchimp_importer.ex index 6b491a87..a9d4573e 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), + {:ok, rider} <- Messaging.send_welcome_sms(rider) do + {:ok, rider} + end end end 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] diff --git a/test/bike_brigade/importers/mailchimp_importer_test.exs b/test/bike_brigade/importers/mailchimp_importer_test.exs index 43a0701c..cd81be29 100644 --- a/test/bike_brigade/importers/mailchimp_importer_test.exs +++ b/test/bike_brigade/importers/mailchimp_importer_test.exs @@ -7,10 +7,11 @@ defmodule BikeBrigade.Tasks.MailchimpImporterTest do alias BikeBrigade.SlackApi.FakeSlack alias BikeBrigade.Riders + alias BikeBrigade.Messaging @list_id "LIST_ID" - @valid_attrs %{ + @member %{ id: "123", email_address: "dispatcher@example.com", status: "subscribed", @@ -25,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) @@ -61,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 @@ -104,19 +105,32 @@ 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, [@member]) + + assert {:ok, _} = MailchimpImporter.sync_riders() + + rider = Riders.get_rider_by_email(@member[:email_address]) + 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]) + 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