From aba3743c1d77a064b33af0558f066b25413630ed Mon Sep 17 00:00:00 2001 From: Nikola Begedin Date: Thu, 21 Dec 2017 11:35:00 +0100 Subject: [PATCH 01/20] Add necessary key to .env.example --- .env.example | 1 + 1 file changed, 1 insertion(+) diff --git a/.env.example b/.env.example index 6d9e64099..704215565 100644 --- a/.env.example +++ b/.env.example @@ -21,3 +21,4 @@ export SEGMENT_WRITE_KEY= export SENTRY_DSN= export STRIPE_SECRET_KEY= export STRIPE_PLATFORM_CLIENT_ID= +export SPARKPOST_API_KEY= From 6fc43595db45abf01eccc40dc4bf99df4d08f5bf Mon Sep 17 00:00:00 2001 From: Nikola Begedin Date: Thu, 21 Dec 2017 13:24:03 +0100 Subject: [PATCH 02/20] Add mix tasks for creating and updating templates Setup basic API usage and testing infrastructure --- config/config.exs | 9 +- config/test.exs | 3 + lib/code_corps/sparkpost/api.ex | 46 +++++++++++ lib/code_corps/sparkpost/extended_api.ex | 39 +++++++++ lib/code_corps/sparkpost/sparkpost.ex | 6 ++ lib/code_corps/sparkpost/tasks.ex | 82 +++++++++++++++++++ lib/mix/tasks/templates/create.ex | 20 +++++ lib/mix/tasks/templates/update.ex | 20 +++++ mix.exs | 1 + mix.lock | 1 + test/lib/code_corps/sparkpost/api_test.exs | 28 +++++++ test/support/sparkpost_testing/failure_api.ex | 31 +++++++ test/support/sparkpost_testing/success_api.ex | 30 +++++++ 13 files changed, 314 insertions(+), 2 deletions(-) create mode 100644 lib/code_corps/sparkpost/api.ex create mode 100644 lib/code_corps/sparkpost/extended_api.ex create mode 100644 lib/code_corps/sparkpost/sparkpost.ex create mode 100644 lib/code_corps/sparkpost/tasks.ex create mode 100644 lib/mix/tasks/templates/create.ex create mode 100644 lib/mix/tasks/templates/update.ex create mode 100644 test/lib/code_corps/sparkpost/api_test.exs create mode 100644 test/support/sparkpost_testing/failure_api.ex create mode 100644 test/support/sparkpost_testing/success_api.ex diff --git a/config/config.exs b/config/config.exs index aa6063423..978a83bba 100644 --- a/config/config.exs +++ b/config/config.exs @@ -14,8 +14,7 @@ config :code_corps, CodeCorpsWeb.Endpoint, url: [host: "localhost"], secret_key_base: "eMl0+Byu0Zv7q48thBu23ChBVFO1+sdLqoMI8yZoxEviF1K3C5uIohbDfvM9felL", render_errors: [view: CodeCorpsWeb.ErrorView, accepts: ~w(html json json-api)], - pubsub: [name: CodeCorps.PubSub, - adapter: Phoenix.PubSub.PG2] + pubsub: [name: CodeCorps.PubSub, adapter: Phoenix.PubSub.PG2] # Configures Elixir's Logger config :logger, :console, @@ -72,6 +71,9 @@ config :code_corps, github_app_client_secret: System.get_env("GITHUB_APP_CLIENT_SECRET"), github_app_pem: pem +config :code_corps, + sparkpost: CodeCorps.SparkPost.ExtendedAPI + config :stripity_stripe, api_key: System.get_env("STRIPE_SECRET_KEY"), connect_client_id: System.get_env("STRIPE_PLATFORM_CLIENT_ID") @@ -81,6 +83,9 @@ config :sentry, enable_source_code_context: true, included_environments: ~w(prod staging)a +config :sparkpost, + api_key: System.get_env("SPARKPOST_API_KEY") + config :code_corps, :sentry, CodeCorps.Sentry.Async config :code_corps, :processor, CodeCorps.Processor.Async diff --git a/config/test.exs b/config/test.exs index a98a8e0c3..3dc4b803f 100644 --- a/config/test.exs +++ b/config/test.exs @@ -57,6 +57,9 @@ config :code_corps, config :sentry, environment_name: Mix.env || :test +config :code_corps, + sparkpost: CodeCorps.SparkPostTesting.SuccessAPI + config :code_corps, :sentry, CodeCorps.Sentry.Sync config :code_corps, :processor, CodeCorps.Processor.Sync diff --git a/lib/code_corps/sparkpost/api.ex b/lib/code_corps/sparkpost/api.ex new file mode 100644 index 000000000..4c5ed8af6 --- /dev/null +++ b/lib/code_corps/sparkpost/api.ex @@ -0,0 +1,46 @@ +defmodule CodeCorps.SparkPost.API do + @moduledoc ~S""" + A wrapper for the SparkPost API. + + All API requests should go through functions in this module. + """ + require Logger + + @type transmission_result :: {:ok, %SparkPost.Transmission.Response{}} | + {:error, %SparkPost.Endpoint.Error{}} + + defp api, do: Application.get_env(:code_corps, :sparkpost) + + def create_template(body \\ %{}, headers \\ [], options \\ []) do + body |> api().create_template(headers, options) |> marshall_response() + end + + def update_template(id, body \\ %{}, headers \\ [], options \\ []) do + id |> api().update_template(body, headers, options) |> marshall_response() + end + + @doc ~S""" + Sends a transmission using the provided email map + """ + @spec send_transmission(%SparkPost.Transmission{}) :: transmission_result + def send_transmission(%SparkPost.Transmission{} = content) do + content |> api().send_transmission() |> marshall_response() + end + + defp marshall_response(%SparkPost.Transmission.Response{} = response), do: {:ok, response} + defp marshall_response(%SparkPost.Content.Inline{} = response), do: {:ok, response} + defp marshall_response(%SparkPost.Endpoint.Error{} = error), do: {:error, error} + # Extended API responses + defp marshall_response({:ok, %HTTPoison.Response{status_code: code} = response}) + when code in 200..399, do: {:ok, response} + defp marshall_response({:ok, %HTTPoison.Response{status_code: code} = response}) + when code in 400..599, do: {:error, response |> build_error()} + defp marshall_response({:error, %HTTPoison.Error{} = error}), do: {:error, error |> build_error()} + + defp build_error(%HTTPoison.Response{status_code: code, body: %{"errors" => errors}}) do + %SparkPost.Endpoint.Error{status_code: code, errors: errors} + end + defp build_error(%HTTPoison.Error{id: id, reason: reason}) do + %SparkPost.Endpoint.Error{status_code: nil, errors: [{id, reason}]} + end +end diff --git a/lib/code_corps/sparkpost/extended_api.ex b/lib/code_corps/sparkpost/extended_api.ex new file mode 100644 index 000000000..d607d688d --- /dev/null +++ b/lib/code_corps/sparkpost/extended_api.ex @@ -0,0 +1,39 @@ +defmodule CodeCorps.SparkPost.ExtendedAPI do + @moduledoc """ + Serves as an extension of the SparkPost API provided by the elixir-sparkpost + library, for features the external package does not support yet + + We should eliminate this module and instead push a PR to add these + features to elixir-sparkpost + """ + + use HTTPoison.Base + + @base_url "https://api.sparkpost.com/api/v1" + + def process_url(url), do: @base_url <> url + + def process_request_headers(headers) do + [ + {"Content-Type", "application/json"}, + {"Authorization", System.get_env("SPARKPOST_API_KEY")} + ] ++ headers + end + + def process_request_body(body), do: body |> Poison.encode! + def process_response_body(body), do: body |> Poison.decode! + + def create_template(body \\ %{}, headers \\ [], options \\ []) do + start() + post("/templates", body, headers, options) + end + + def update_template(id, body \\ %{}, headers \\ [], options \\ []) do + start() + put("/templates/#{id}", body, headers, options) + end + + def send_transmission(content) do + SparkPost.Transmission.send(content) + end +end diff --git a/lib/code_corps/sparkpost/sparkpost.ex b/lib/code_corps/sparkpost/sparkpost.ex new file mode 100644 index 000000000..c3280612c --- /dev/null +++ b/lib/code_corps/sparkpost/sparkpost.ex @@ -0,0 +1,6 @@ +defmodule CodeCorps.SparkPost do + alias CodeCorps.SparkPost.Tasks + + defdelegate create_templates, to: Tasks + defdelegate update_templates, to: Tasks +end diff --git a/lib/code_corps/sparkpost/tasks.ex b/lib/code_corps/sparkpost/tasks.ex new file mode 100644 index 000000000..11712b061 --- /dev/null +++ b/lib/code_corps/sparkpost/tasks.ex @@ -0,0 +1,82 @@ +defmodule CodeCorps.SparkPost.Tasks do + @moduledoc ~S""" + Subcontext holding all commonly performed Tasks related to SparkPost + """ + + @templates [ + "forgot-password", + "message-initiated-by-project", + "organization-invite", + "project-approval-request", + "project-approved", + "project-user-acceptance", + "project-user-request", + "receipt", + "reply-to-conversation" + ] + + alias CodeCorps.SparkPost.API + + @doc ~S""" + Builds a stream, which, when evaluated, makes API requests to create all + supported SparkPost templates. + """ + @spec create_templates :: Enumerable.t() + def create_templates do + @templates + |> Enum.map(fn id -> {id, id |> build_payload} end) + |> Enum.map(fn {id, payload} -> {id, payload |> Map.put(:id, id)} end) + |> Stream.map(fn {id, payload} -> {id, payload |> API.create_template()} end) + end + + @default_params [params: %{update_published: true}] + + @doc ~S""" + Builds a stream, which, when evaluated, makes API requests to update all + supported SparkPost templates. + """ + @spec create_templates :: Enumerable.t() + def update_templates do + @templates + |> Enum.map(fn id -> {id, id |> build_payload} end) + |> Stream.map(fn {id, payload} -> {id, id |> API.update_template(payload, [], @default_params)} end) + end + + @spec build_payload(String.t) :: map + defp build_payload(id) do + %{ + published: true, + content: %{ + from: %{email: "{{from_email}}", name: "{{from_name}}"}, + html: id |> load_template() |> insert_styles(), + subject: "{{subject}}" + }, + options: %{inline_css: true, transactional: true} + } + end + + @spec load_template(String.t) :: String.t + def load_template(id) do + File.cwd! |> Path.join("emails") |> Path.join("#{id |> Inflex.underscore}.html") |> File.read! + end + + @linked_style_element "" + + @spec insert_styles(String.t) :: String.t + defp insert_styles(template_content) do + styles = load_css_file() + + style_element = """ + + """ + + template_content |> String.replace(@linked_style_element, style_element) + end + + @spec load_css_file :: String.t + defp load_css_file() do + File.cwd! |> Path.join("emails") |> Path.join("styles.css") |> File.read! + end +end diff --git a/lib/mix/tasks/templates/create.ex b/lib/mix/tasks/templates/create.ex new file mode 100644 index 000000000..aa41dd456 --- /dev/null +++ b/lib/mix/tasks/templates/create.ex @@ -0,0 +1,20 @@ +defmodule Mix.Tasks.Templates.Create do + @moduledoc """ + Initializes blank templates on SparkPost. + Should only need to be caled once per environment + """ + use Mix.Task + + require Logger + + def run(_) do + CodeCorps.SparkPost.create_templates() + |> Enum.each(&log_result(&1)) + end + + defp log_result({id, {:ok, _result}}), do: Logger.log(:info, "Template #{id} created sucessfuly.") + defp log_result({id, {:error, error}}) do + Logger.log(:error, "Failed to create template #{id}. The response was:") + Logger.log(:warn, Kernel.inspect(error, pretty: true)) + end +end diff --git a/lib/mix/tasks/templates/update.ex b/lib/mix/tasks/templates/update.ex new file mode 100644 index 000000000..a89752088 --- /dev/null +++ b/lib/mix/tasks/templates/update.ex @@ -0,0 +1,20 @@ +defmodule Mix.Tasks.Templates.Update do + @moduledoc """ + Updates SparkPost templates with template data created locally. + Should only be called whenever a template needs to be changed. + """ + use Mix.Task + + require Logger + + def run(_) do + CodeCorps.SparkPost.update_templates() + |> Enum.each(&log_result(&1)) + end + + defp log_result({id, {:ok, _result}}), do: Logger.log(:info, "Template #{id} updated sucessfuly.") + defp log_result({id, {:error, error}}) do + Logger.log(:error, "Failed to update template #{id}. The response was:") + Logger.log(:warn, Kernel.inspect(error, pretty: true)) + end +end diff --git a/mix.exs b/mix.exs index b5b4a2855..f0a2b08b9 100644 --- a/mix.exs +++ b/mix.exs @@ -78,6 +78,7 @@ defmodule CodeCorps.Mixfile do {:scrivener_ecto, "~> 1.2"}, # DB query pagination {:segment, "~> 0.1"}, # Segment analytics {:sentry, "~> 6.0"}, # Sentry error tracking + {:sparkpost, "~> 0.5"}, {:stripity_stripe, git: "https://github.com/code-corps/stripity_stripe.git", branch: "2.0-beta"}, # Stripe {:sweet_xml, "~> 0.5"}, {:timber, "~> 2.0"}, # Logging diff --git a/mix.lock b/mix.lock index 9047047d8..59dc712de 100644 --- a/mix.lock +++ b/mix.lock @@ -60,6 +60,7 @@ "scrivener_ecto": {:hex, :scrivener_ecto, "1.3.0", "69698428e22810ac8a47abc12d1df5b2f5d8f6b36dc5d5bfe6dd93fde857c576", [:mix], [{:ecto, "~> 2.0", [hex: :ecto, optional: false]}, {:postgrex, "~> 0.11.0 or ~> 0.12.0 or ~> 0.13.0", [hex: :postgrex, optional: true]}, {:scrivener, "~> 2.4", [hex: :scrivener, optional: false]}]}, "segment": {:hex, :segment, "0.1.1", "47bf9191590e7a533c105d1e21518e0d6da47c91e8d98ebb649c624db5dfc359", [:mix], [{:httpoison, "~> 0.8", [hex: :httpoison, optional: false]}, {:poison, "~> 1.3 or ~> 2.0", [hex: :poison, optional: false]}]}, "sentry": {:hex, :sentry, "6.0.4", "b7f93e77cdc2a44a55ec2bec886c3eeab7d562316fecd3000c04b68d1a6b0392", [:mix], [{:hackney, "~> 1.8 or 1.6.5", [hex: :hackney, optional: false]}, {:plug, "~> 1.0", [hex: :plug, optional: true]}, {:poison, "~> 1.5 or ~> 2.0 or ~> 3.0", [hex: :poison, optional: false]}, {:uuid, "~> 1.0", [hex: :uuid, optional: false]}]}, + "sparkpost": {:hex, :sparkpost, "0.5.1", "59d85a140fa4d5d40ee8ec970cf38fe5f7661a940595b16d565439c88e39fd52", [], [{:httpoison, "~> 0.9", [hex: :httpoison, repo: "hexpm", optional: false]}, {:poison, "~> 2.0 or ~> 3.0", [hex: :poison, repo: "hexpm", optional: false]}], "hexpm"}, "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.1", "28a4d65b7f59893bc2c7de786dec1e1555bd742d336043fe644ae956c3497fbe", [:make, :rebar], []}, "stripity_stripe": {:git, "https://github.com/code-corps/stripity_stripe.git", "1493e42eb5bd39b135a6686912a20ce200d07375", [branch: "2.0-beta"]}, "sweet_xml": {:hex, :sweet_xml, "0.6.5", "dd9cde443212b505d1b5f9758feb2000e66a14d3c449f04c572f3048c66e6697", [:mix], []}, diff --git a/test/lib/code_corps/sparkpost/api_test.exs b/test/lib/code_corps/sparkpost/api_test.exs new file mode 100644 index 000000000..06af8a01a --- /dev/null +++ b/test/lib/code_corps/sparkpost/api_test.exs @@ -0,0 +1,28 @@ +defmodule CodeCorps.SparkPost.APITest do + @moduledoc false + + use ExUnit.Case, async: false + + alias CodeCorps.SparkPost.API + + describe "create_template/3" do + test "works" do + API.create_template(%{id: "id"}, "foo", "bar") + assert_received({%{id: "id"}, "foo", "bar"}) + end + end + + describe "update_template/4" do + test "works" do + API.update_template("id", "foo", "bar", "baz") + assert_received({"id", "foo", "bar", "baz"}) + end + end + + describe "send_transmission/1" do + test "works" do + API.send_transmission(%SparkPost.Transmission{recipients: []}) + assert_received(%SparkPost.Transmission{recipients: []}) + end + end +end diff --git a/test/support/sparkpost_testing/failure_api.ex b/test/support/sparkpost_testing/failure_api.ex new file mode 100644 index 000000000..b4d245e41 --- /dev/null +++ b/test/support/sparkpost_testing/failure_api.ex @@ -0,0 +1,31 @@ +defmodule CodeCorps.SparkPostTesting.FailureAPI do + @moduledoc """ + Basic testing module which servers as a mock for the SparkPost api. All + responses should be some form of a failure response. + """ + + @bad_request %SparkPost.Endpoint.Error{ + errors: [%{message: "Bad request"}], + status_code: 400 + } + + def create_template(body, headers, options) do + send(self(), {body, headers, options}) + @bad_request + end + + def update_template(id, body, headers, options) do + send(self(), {id, body, headers, options}) + @bad_request + end + + def preview_template(template_ref, substitution_data) do + send(self(), {template_ref, substitution_data}) + @bad_request + end + + def send_transmission(%SparkPost.Transmission{} = transmission) do + send(self(), transmission) + @bad_request + end +end diff --git a/test/support/sparkpost_testing/success_api.ex b/test/support/sparkpost_testing/success_api.ex new file mode 100644 index 000000000..bb3b2a322 --- /dev/null +++ b/test/support/sparkpost_testing/success_api.ex @@ -0,0 +1,30 @@ +defmodule CodeCorps.SparkPostTesting.SuccessAPI do + @moduledoc """ + Basic testing module which servers as a mock for the SparkPost api. All + responses should be default success responses. + """ + def create_template(body, headers, options) do + send(self(), {body, headers, options}) + {:ok, %HTTPoison.Response{status_code: 200}} + end + + def update_template(id, body, headers, options) do + send(self(), {id, body, headers, options}) + {:ok, %HTTPoison.Response{status_code: 200}} + end + + def preview_template(template_ref, substitution_data) do + send(self(), {template_ref, substitution_data}) + %SparkPost.Content.Inline{html: ""} + end + + def send_transmission(%SparkPost.Transmission{recipients: recipients} = transmission) do + send(self(), transmission) + + %SparkPost.Transmission.Response{ + id: 1, + total_accepted_recipients: recipients |> Enum.count, + total_rejected_recipients: 0 + } + end +end From fa7c018d6edd2c229957033bc30d7633e6a76d0a Mon Sep 17 00:00:00 2001 From: Nikola Begedin Date: Thu, 21 Dec 2017 17:51:19 +0100 Subject: [PATCH 03/20] Switch forgot password email to sparkpost --- .../emails/forgot_password_email.ex | 23 ------------- lib/code_corps/services/forgot_password.ex | 6 ++-- lib/code_corps/sparkpost/emails/emails.ex | 7 ++++ .../sparkpost/emails/forgot_password.ex | 26 ++++++++++++++ lib/code_corps/sparkpost/sparkpost.ex | 4 ++- .../emails/forgot_password_email_test.exs | 17 ---------- .../sparkpost/emails/forgot_password_test.exs | 31 +++++++++++++++++ .../controllers/password_controller_test.exs | 11 +++--- test/support/sparkpost_helpers.ex | 34 +++++++++++++++++++ 9 files changed, 110 insertions(+), 49 deletions(-) delete mode 100644 lib/code_corps/emails/forgot_password_email.ex create mode 100644 lib/code_corps/sparkpost/emails/emails.ex create mode 100644 lib/code_corps/sparkpost/emails/forgot_password.ex delete mode 100644 test/lib/code_corps/emails/forgot_password_email_test.exs create mode 100644 test/lib/code_corps/sparkpost/emails/forgot_password_test.exs create mode 100644 test/support/sparkpost_helpers.ex diff --git a/lib/code_corps/emails/forgot_password_email.ex b/lib/code_corps/emails/forgot_password_email.ex deleted file mode 100644 index 0bd8bdfec..000000000 --- a/lib/code_corps/emails/forgot_password_email.ex +++ /dev/null @@ -1,23 +0,0 @@ -defmodule CodeCorps.Emails.ForgotPasswordEmail do - import Bamboo.Email, only: [to: 2] - import Bamboo.PostmarkHelper - - alias CodeCorps.{Emails.BaseEmail, User, WebClient} - - @spec create(User.t, String.t) :: Bamboo.Email.t - def create(%User{} = user, token) do - BaseEmail.create - |> to(user.email) - |> template(template_id(), %{link: link(token)}) - end - - @spec template_id :: String.t - defp template_id, do: Application.get_env(:code_corps, :postmark_forgot_password_template) - - @spec link(String.t) :: String.t - defp link(token) do - WebClient.url() - |> URI.merge("password/reset?token=#{token}") - |> URI.to_string - end -end diff --git a/lib/code_corps/services/forgot_password.ex b/lib/code_corps/services/forgot_password.ex index 0489e4575..5480c489c 100644 --- a/lib/code_corps/services/forgot_password.ex +++ b/lib/code_corps/services/forgot_password.ex @@ -1,16 +1,16 @@ defmodule CodeCorps.Services.ForgotPasswordService do # credo:disable-for-this-file Credo.Check.Refactor.PipeChainStart - alias CodeCorps.{AuthToken, Emails, Mailer, Repo, User} + alias CodeCorps.{AuthToken, Repo, SparkPost, User} @doc""" Generates an AuthToken model and sends to the provided email. """ def forgot_password(email) do with %User{} = user <- Repo.get_by(User, email: email), - { :ok, %AuthToken{} = %{ value: token } } <- AuthToken.changeset(%AuthToken{}, user) |> Repo.insert + {:ok, %AuthToken{value: token}} <- AuthToken.changeset(%AuthToken{}, user) |> Repo.insert do - Emails.ForgotPasswordEmail.create(user, token) |> Mailer.deliver_now() + user |> SparkPost.send_forgot_password_email(token) else nil -> nil end diff --git a/lib/code_corps/sparkpost/emails/emails.ex b/lib/code_corps/sparkpost/emails/emails.ex new file mode 100644 index 000000000..4fa574d98 --- /dev/null +++ b/lib/code_corps/sparkpost/emails/emails.ex @@ -0,0 +1,7 @@ +defmodule CodeCorps.SparkPost.Emails do + alias CodeCorps.SparkPost.{API, Emails} + + def send_forgot_password_email(user, token) do + user |> Emails.ForgotPassword.build(token) |> API.send_transmission + end +end diff --git a/lib/code_corps/sparkpost/emails/forgot_password.ex b/lib/code_corps/sparkpost/emails/forgot_password.ex new file mode 100644 index 000000000..765c32c37 --- /dev/null +++ b/lib/code_corps/sparkpost/emails/forgot_password.ex @@ -0,0 +1,26 @@ +defmodule CodeCorps.SparkPost.Emails.ForgotPassword do + alias CodeCorps.{User, WebClient} + alias SparkPost.{Content, Transmission} + + @spec build(User.t, String.t) :: %Transmission{} + def build(%User{} = user, token) do + %Transmission{ + content: %Content.TemplateRef{template_id: "forgot-password"}, + options: %Transmission.Options{inline_css: true}, + recipients: [%{name: user.first_name, email: user.email}], + substitution_data: %{ + from_name: "Code Corps", + from_email: "team@codecorps.org", + link: link(token), + subject: "Here's the link to reset your password" + } + } + end + + @spec link(String.t) :: String.t + defp link(token) do + WebClient.url() + |> URI.merge("password/reset?token=#{token}") + |> URI.to_string + end +end diff --git a/lib/code_corps/sparkpost/sparkpost.ex b/lib/code_corps/sparkpost/sparkpost.ex index c3280612c..198afcb56 100644 --- a/lib/code_corps/sparkpost/sparkpost.ex +++ b/lib/code_corps/sparkpost/sparkpost.ex @@ -1,6 +1,8 @@ defmodule CodeCorps.SparkPost do - alias CodeCorps.SparkPost.Tasks + alias CodeCorps.SparkPost.{Emails, Tasks} defdelegate create_templates, to: Tasks defdelegate update_templates, to: Tasks + + defdelegate send_forgot_password_email(user, token), to: Emails end diff --git a/test/lib/code_corps/emails/forgot_password_email_test.exs b/test/lib/code_corps/emails/forgot_password_email_test.exs deleted file mode 100644 index 1dfe73ae0..000000000 --- a/test/lib/code_corps/emails/forgot_password_email_test.exs +++ /dev/null @@ -1,17 +0,0 @@ -defmodule CodeCorps.Emails.ForgotPasswordEmailTest do - use CodeCorps.ModelCase - use Bamboo.Test - - alias CodeCorps.{AuthToken, Emails.ForgotPasswordEmail, WebClient} - - test "forgot password email works" do - user = insert(:user) - { :ok, %AuthToken{ value: token } } = AuthToken.changeset(%AuthToken{}, user) |> Repo.insert - - email = ForgotPasswordEmail.create(user, token) - assert email.from == "Code Corps" - assert email.to == user.email - { :link, encoded_link } = email.private.template_model |> Enum.at(0) - assert "#{WebClient.url()}/password/reset?token=#{token}" == encoded_link - end -end diff --git a/test/lib/code_corps/sparkpost/emails/forgot_password_test.exs b/test/lib/code_corps/sparkpost/emails/forgot_password_test.exs new file mode 100644 index 000000000..d7b12a4de --- /dev/null +++ b/test/lib/code_corps/sparkpost/emails/forgot_password_test.exs @@ -0,0 +1,31 @@ +defmodule CodeCorps.SparkPost.Emails.ForgotPasswordTest do + use CodeCorps.DbAccessCase + + alias CodeCorps.{SparkPost.Emails.ForgotPassword, WebClient} + + describe "build/2" do + test "provides substitution data for all keys used by template" do + user = insert(:user) + token = "foo" + %{substitution_data: data} = ForgotPassword.build(user, token) + + expected_keys = "forgot-password" |> CodeCorps.SparkPostHelpers.get_keys_used_by_template + assert data |> Map.keys == expected_keys + end + + test "builds correct transmission model" do + user = insert(:user) + token = "foo" + + %{substitution_data: data, recipients: [recipient]} = + ForgotPassword.build(user, token) + + assert data.from_name == "Code Corps" + assert data.from_email == "team@codecorps.org" + assert data.link == "#{WebClient.url()}/password/reset?token=#{token}" + + assert recipient.email == user.email + assert recipient.name == user.first_name + end + end +end diff --git a/test/lib/code_corps_web/controllers/password_controller_test.exs b/test/lib/code_corps_web/controllers/password_controller_test.exs index 08f8942b5..f3c26fbad 100644 --- a/test/lib/code_corps_web/controllers/password_controller_test.exs +++ b/test/lib/code_corps_web/controllers/password_controller_test.exs @@ -15,7 +15,8 @@ defmodule CodeCorpsWeb.PasswordControllerTest do assert response == %{ "email" => user.email } %AuthToken{value: token} = Repo.get_by(AuthToken, user_id: user.id) - assert_delivered_email CodeCorps.Emails.ForgotPasswordEmail.create(user, token) + expected_email = CodeCorps.SparkPost.Emails.ForgotPassword.build(user, token) + assert_received ^expected_email end @tag :authenticated @@ -28,20 +29,20 @@ defmodule CodeCorpsWeb.PasswordControllerTest do assert response == %{ "email" => user.email } %AuthToken{value: token} = Repo.get_by(AuthToken, user_id: user.id) - assert_delivered_email CodeCorps.Emails.ForgotPasswordEmail.create(user, token) + expected_email = CodeCorps.SparkPost.Emails.ForgotPassword.build(user, token) + assert_received ^expected_email refute CodeCorps.Guardian.Plug.authenticated?(conn) end test "does not create resource and renders 200 when email is invalid", %{conn: conn} do - user = insert(:user) + insert(:user) attrs = %{"email" => "random_email@gmail.com"} conn = post conn, password_path(conn, :forgot_password), attrs response = json_response(conn, 200) assert response == %{ "email" => "random_email@gmail.com" } - refute_delivered_email CodeCorps.Emails.ForgotPasswordEmail.create(user, nil) + refute_received %SparkPost.Transmission{} end - end diff --git a/test/support/sparkpost_helpers.ex b/test/support/sparkpost_helpers.ex new file mode 100644 index 000000000..b6f9687bc --- /dev/null +++ b/test/support/sparkpost_helpers.ex @@ -0,0 +1,34 @@ +defmodule CodeCorps.SparkPostHelpers do + @moduledoc """ + Holds helpers used for various Sparkpost related tests + """ + + @reserved_keys [:else, :end, :if, :for] + @global_keys [:subject, :from_name, :from_email] + + @doc ~S""" + Scans a SparkPost email template and returns all keys used by the template, + as atoms. + + This includes keys which would have been provided by the recipient. + + + Use `remove_recipient_keys/1` to drop those. + """ + @spec get_keys_used_by_template(String.t) :: list(Atom.t) + def get_keys_used_by_template(id) do + File.cwd! + |> Path.join("emails") + |> Path.join("#{id |> Inflex.underscore}.html") + |> File.read! + |> (fn template -> Regex.scan(~r/{{\s*[\w_]+\s*}}/, template) end).() + |> List.flatten + |> Enum.map(&Regex.scan(~r/[\w\.]+/, &1)) + |> List.flatten + |> Enum.uniq + |> Enum.map(&String.to_existing_atom/1) + |> (fn list -> list -- @reserved_keys end).() + |> Enum.concat(@global_keys) + |> Enum.sort + end +end From 3c3b7884d0aa7cc4a2cb28ab1b86603975aabaec Mon Sep 17 00:00:00 2001 From: Nikola Begedin Date: Fri, 22 Dec 2017 08:55:25 +0100 Subject: [PATCH 04/20] Switch receipt email to SparkPost --- lib/code_corps/emails/receipt_email.ex | 93 ----------------- lib/code_corps/sparkpost/emails/emails.ex | 8 ++ lib/code_corps/sparkpost/emails/receipt.ex | 85 ++++++++++++++++ lib/code_corps/sparkpost/sparkpost.ex | 1 + .../events/connect_charge_succeeded.ex | 34 +++---- priv/repo/structure.sql | 4 +- .../code_corps/emails/receipt_email_test.exs | 57 ----------- .../sparkpost/emails/receipt_test.exs | 99 +++++++++++++++++++ .../events/connect_charge_succeeded_test.exs | 9 +- 9 files changed, 212 insertions(+), 178 deletions(-) delete mode 100644 lib/code_corps/emails/receipt_email.ex create mode 100644 lib/code_corps/sparkpost/emails/receipt.ex delete mode 100644 test/lib/code_corps/emails/receipt_email_test.exs create mode 100644 test/lib/code_corps/sparkpost/emails/receipt_test.exs diff --git a/lib/code_corps/emails/receipt_email.ex b/lib/code_corps/emails/receipt_email.ex deleted file mode 100644 index cc49cc6fb..000000000 --- a/lib/code_corps/emails/receipt_email.ex +++ /dev/null @@ -1,93 +0,0 @@ -defmodule CodeCorps.Emails.ReceiptEmail do - import Bamboo.Email, only: [to: 2] - import Bamboo.PostmarkHelper - - alias CodeCorps.Emails.BaseEmail - alias CodeCorps.{DonationGoal, Project, Repo, StripeConnectCharge, StripeConnectSubscription, WebClient} - - @spec create(StripeConnectCharge.t, Stripe.Invoice.t) :: Bamboo.Email.t - def create(%StripeConnectCharge{} = charge, %Stripe.Invoice{} = invoice) do - with %StripeConnectCharge{} = charge <- Repo.preload(charge, :user), - %Project{} = project <- get_project(invoice.subscription), - {:ok, %DonationGoal{} = current_donation_goal} <- get_current_donation_goal(project), - template_model <- build_model(charge, project, current_donation_goal) - do - BaseEmail.create - |> to(charge.user.email) - |> template(template_id(), template_model) - else - nil -> {:error, :project_not_found} - other -> other - end - end - - @spec get_project(String.t) :: Project.t | {:error, :subscription_not_found} - defp get_project(subscription_id_from_stripe) do - with %StripeConnectSubscription{} = subscription <- get_subscription(subscription_id_from_stripe) do - subscription.stripe_connect_plan.project - else - nil -> {:error, :subscription_not_found} - end - end - - @spec get_subscription(String.t) :: StripeConnectSubscription.t | nil - defp get_subscription(subscription_id_from_stripe) do - StripeConnectSubscription - |> Repo.get_by(id_from_stripe: subscription_id_from_stripe) - |> Repo.preload(stripe_connect_plan: [project: :organization]) - end - - @spec get_current_donation_goal(Project.t) :: DonationGoal.t | {:error, :donation_goal_not_found} - defp get_current_donation_goal(project) do - case Repo.get_by(DonationGoal, current: true, project_id: project.id) do - nil -> {:error, :donation_goal_not_found} - donation_goal -> {:ok, donation_goal} - end - end - - @spec build_model(StripeConnectCharge.t, Project.t, DonationGoal.t) :: map - defp build_model(charge, project, current_donation_goal) do - %{ - charge_amount: charge.amount |> format_amount(), - charge_statement_descriptor: charge.statement_descriptor, - high_five_image_url: high_five_image_url(), - name: BaseEmail.get_name(charge.user), - project_current_donation_goal_description: current_donation_goal.description, - project_title: project.title, - project_url: project |> url(), - subject: project |> build_subject_line() - } - end - - @spec build_subject_line(Project.t) :: String.t - defp build_subject_line(project) do - "Your monthly donation to " <> project.title - end - - @spec high_five_image_url :: String.t - defp high_five_image_url, do: Enum.random(high_five_image_urls()) - - @spec high_five_image_urls :: list(String.t) - defp high_five_image_urls, do: [ - "https://d3pgew4wbk2vb1.cloudfront.net/emails/images/emoji-1f64c-1f3fb@2x.png", - "https://d3pgew4wbk2vb1.cloudfront.net/emails/images/emoji-1f64c-1f3fc@2x.png", - "https://d3pgew4wbk2vb1.cloudfront.net/emails/images/emoji-1f64c-1f3fd@2x.png", - "https://d3pgew4wbk2vb1.cloudfront.net/emails/images/emoji-1f64c-1f3fe@2x.png", - "https://d3pgew4wbk2vb1.cloudfront.net/emails/images/emoji-1f64c-1f3ff@2x.png" - ] - - @spec format_amount(integer) :: binary - defp format_amount(amount) do - amount |> Money.new(:USD) |> Money.to_string() - end - - @spec url(Project.t) :: String.t - defp url(project) do - WebClient.url() - |> URI.merge(project.organization.slug <> "/" <> project.slug) - |> URI.to_string - end - - @spec template_id :: String.t - defp template_id, do: Application.get_env(:code_corps, :postmark_receipt_template) -end diff --git a/lib/code_corps/sparkpost/emails/emails.ex b/lib/code_corps/sparkpost/emails/emails.ex index 4fa574d98..ea465d997 100644 --- a/lib/code_corps/sparkpost/emails/emails.ex +++ b/lib/code_corps/sparkpost/emails/emails.ex @@ -4,4 +4,12 @@ defmodule CodeCorps.SparkPost.Emails do def send_forgot_password_email(user, token) do user |> Emails.ForgotPassword.build(token) |> API.send_transmission end + + def send_receipt_email(charge, invoice) do + case charge |> Emails.Receipt.build(invoice) do + %SparkPost.Transmission{} = transmission -> + transmission |> API.send_transmission + build_failure -> build_failure + end + end end diff --git a/lib/code_corps/sparkpost/emails/receipt.ex b/lib/code_corps/sparkpost/emails/receipt.ex new file mode 100644 index 000000000..39643add6 --- /dev/null +++ b/lib/code_corps/sparkpost/emails/receipt.ex @@ -0,0 +1,85 @@ +defmodule CodeCorps.SparkPost.Emails.Receipt do + + alias CodeCorps.{ + DonationGoal, + Project, + Repo, + StripeConnectCharge, + StripeConnectSubscription, + User, + WebClient + } + alias SparkPost.{Content, Transmission} + + @high_five_image_urls [ + "https://d3pgew4wbk2vb1.cloudfront.net/emails/images/emoji-1f64c-1f3fb@2x.png", + "https://d3pgew4wbk2vb1.cloudfront.net/emails/images/emoji-1f64c-1f3fc@2x.png", + "https://d3pgew4wbk2vb1.cloudfront.net/emails/images/emoji-1f64c-1f3fd@2x.png", + "https://d3pgew4wbk2vb1.cloudfront.net/emails/images/emoji-1f64c-1f3fe@2x.png", + "https://d3pgew4wbk2vb1.cloudfront.net/emails/images/emoji-1f64c-1f3ff@2x.png" + ] + + @spec build(StripeConnectCharge.t, Stripe.Invoice.t) :: %Transmission{} + def build(%StripeConnectCharge{} = charge, %Stripe.Invoice{} = invoice) do + with %StripeConnectCharge{user: %User{} = user} = charge <- Repo.preload(charge, :user), + %Project{} = project <- invoice.subscription |> get_project(), + {:ok, %DonationGoal{} = current_donation_goal} <- project |> get_current_donation_goal() + do + %Transmission{ + content: %Content.TemplateRef{template_id: "receipt"}, + options: %Transmission.Options{inline_css: true}, + recipients: [%{name: user.first_name, email: user.email}], + substitution_data: %{ + charge_amount: charge.amount |> Money.new(:USD) |> Money.to_string(), + charge_statement_descriptor: charge.statement_descriptor, + from_name: "Code Corps", + from_email: "team@codecorps.org", + high_five_image_url: Enum.random(@high_five_image_urls), + name: charge.user |> get_name(), + project_current_donation_goal_description: current_donation_goal.description, + project_title: project.title, + project_url: project |> url(), + subject: "Your monthly donation to #{project.title}" + } + } + else + nil -> {:error, :project_not_found} + other -> other + end + end + + @spec get_project(String.t) :: Project.t | {:error, :subscription_not_found} + defp get_project(subscription_id_from_stripe) do + with %StripeConnectSubscription{} = subscription <- get_subscription(subscription_id_from_stripe) do + subscription.stripe_connect_plan.project + else + nil -> {:error, :subscription_not_found} + end + end + + @spec get_subscription(String.t) :: StripeConnectSubscription.t | nil + defp get_subscription(subscription_id_from_stripe) do + StripeConnectSubscription + |> Repo.get_by(id_from_stripe: subscription_id_from_stripe) + |> Repo.preload(stripe_connect_plan: [project: :organization]) + end + + @spec get_current_donation_goal(Project.t) :: DonationGoal.t | {:error, :donation_goal_not_found} + defp get_current_donation_goal(project) do + case Repo.get_by(DonationGoal, current: true, project_id: project.id) do + nil -> {:error, :donation_goal_not_found} + donation_goal -> {:ok, donation_goal} + end + end + + @spec url(Project.t) :: String.t + defp url(project) do + WebClient.url() + |> URI.merge(project.organization.slug <> "/" <> project.slug) + |> URI.to_string + end + + @spec get_name(User.t) :: String.t + defp get_name(%User{first_name: nil}), do: "there" + defp get_name(%User{first_name: name}), do: name +end diff --git a/lib/code_corps/sparkpost/sparkpost.ex b/lib/code_corps/sparkpost/sparkpost.ex index 198afcb56..6c2a3bcea 100644 --- a/lib/code_corps/sparkpost/sparkpost.ex +++ b/lib/code_corps/sparkpost/sparkpost.ex @@ -5,4 +5,5 @@ defmodule CodeCorps.SparkPost do defdelegate update_templates, to: Tasks defdelegate send_forgot_password_email(user, token), to: Emails + defdelegate send_receipt_email(charge, invoice), to: Emails end diff --git a/lib/code_corps/stripe_service/events/connect_charge_succeeded.ex b/lib/code_corps/stripe_service/events/connect_charge_succeeded.ex index 8cb8c2f87..6c27db325 100644 --- a/lib/code_corps/stripe_service/events/connect_charge_succeeded.ex +++ b/lib/code_corps/stripe_service/events/connect_charge_succeeded.ex @@ -3,41 +3,37 @@ defmodule CodeCorps.StripeService.Events.ConnectChargeSucceeded do Performs everything required to handle a charge.succeeded webhook on Stripe Connect """ - alias CodeCorps.StripeService.StripeConnectChargeService - alias CodeCorps.{Emails, Mailer, StripeConnectCharge} + + alias SparkPost.Transmission + alias CodeCorps.{ + SparkPost, + StripeService.StripeConnectChargeService, + StripeConnectCharge + } @api Application.get_env(:code_corps, :stripe) def handle(%{data: %{object: %{id: id_from_stripe}}, user_id: connect_account_id_from_stripe}) do with {:ok, %StripeConnectCharge{} = charge} <- StripeConnectChargeService.create(id_from_stripe, connect_account_id_from_stripe) do charge |> track_created() - - charge - |> try_create_receipt(connect_account_id_from_stripe) - |> maybe_send_receipt() + charge |> try_send_receipt(connect_account_id_from_stripe) else failure -> failure end end - defp try_create_receipt(%StripeConnectCharge{invoice_id_from_stripe: invoice_id} = charge, account_id) do + defp track_created(%StripeConnectCharge{user_id: user_id} = charge) do + CodeCorps.Analytics.SegmentTracker.track(user_id, :create, charge) + end + + defp try_send_receipt(%StripeConnectCharge{invoice_id_from_stripe: invoice_id} = charge, account_id) do with {:ok, %Stripe.Invoice{} = invoice} <- @api.Invoice.retrieve(invoice_id, connect_account: account_id), - %Bamboo.Email{} = receipt <- Emails.ReceiptEmail.create(charge, invoice) + {:ok, %Transmission.Response{} = response} <- charge |> SparkPost.send_receipt_email(invoice) do - {:ok, charge, receipt} + {:ok, charge, response} else failure -> failure end end - defp maybe_send_receipt({:ok, charge, receipt}) do - with %Bamboo.Email{} = email <- receipt |> Mailer.deliver_now do - {:ok, charge, email} - end - end - defp maybe_send_receipt(other), do: other - - defp track_created(%StripeConnectCharge{user_id: user_id} = charge) do - CodeCorps.Analytics.SegmentTracker.track(user_id, :create, charge) - end end diff --git a/priv/repo/structure.sql b/priv/repo/structure.sql index cfe9bc6e9..f69d78109 100644 --- a/priv/repo/structure.sql +++ b/priv/repo/structure.sql @@ -2,8 +2,8 @@ -- PostgreSQL database dump -- --- Dumped from database version 10.1 --- Dumped by pg_dump version 10.1 +-- Dumped from database version 10.0 +-- Dumped by pg_dump version 10.0 SET statement_timeout = 0; SET lock_timeout = 0; diff --git a/test/lib/code_corps/emails/receipt_email_test.exs b/test/lib/code_corps/emails/receipt_email_test.exs deleted file mode 100644 index e71db399a..000000000 --- a/test/lib/code_corps/emails/receipt_email_test.exs +++ /dev/null @@ -1,57 +0,0 @@ -defmodule CodeCorps.Emails.ReceiptEmailTest do - use CodeCorps.ModelCase - use Bamboo.Test - - alias CodeCorps.Emails.ReceiptEmail - - test "receipt email works" do - invoice_fixture = CodeCorps.StripeTesting.Helpers.load_fixture("invoice") - - user = insert(:user, email: "jimmy@mail.com", first_name: "Jimmy") - - project = insert(:project, title: "Code Corps") - plan = insert(:stripe_connect_plan, project: project) - subscription = insert( - :stripe_connect_subscription, - id_from_stripe: invoice_fixture.subscription, - stripe_connect_plan: plan, - user: user - ) - - invoice = insert( - :stripe_invoice, - id_from_stripe: invoice_fixture.id, - stripe_connect_subscription: subscription, - user: user - ) - - charge = insert( - :stripe_connect_charge, - amount: 500, - id_from_stripe: invoice.charge_id_from_stripe, - invoice_id_from_stripe: invoice.id_from_stripe, - user: user, - statement_descriptor: "Test descriptor" - ) - - insert(:donation_goal, project: project, current: true, description: "Test goal") - - email = ReceiptEmail.create(charge, invoice_fixture) - assert email.from == "Code Corps" - assert email.to == "jimmy@mail.com" - - template_model = email.private.template_model |> Map.delete(:high_five_image_url) - high_five_image_url = email.private.template_model |> Map.get(:high_five_image_url) - - assert template_model == %{ - charge_amount: "$5.00", - charge_statement_descriptor: "Test descriptor", - name: "Jimmy", - project_title: "Code Corps", - project_url: "http://localhost:4200/#{project.organization.slug}/#{project.slug}", - project_current_donation_goal_description: "Test goal", - subject: "Your monthly donation to Code Corps" - } - assert high_five_image_url - end -end diff --git a/test/lib/code_corps/sparkpost/emails/receipt_test.exs b/test/lib/code_corps/sparkpost/emails/receipt_test.exs new file mode 100644 index 000000000..2e15343e8 --- /dev/null +++ b/test/lib/code_corps/sparkpost/emails/receipt_test.exs @@ -0,0 +1,99 @@ +defmodule CodeCorps.SparkPost.Emails.ReceiptTest do + use CodeCorps.DbAccessCase + + alias CodeCorps.SparkPost.Emails.Receipt + + describe "build/2" do + test "provides substitution data for all keys used by template" do + invoice_fixture = CodeCorps.StripeTesting.Helpers.load_fixture("invoice") + + user = insert(:user, email: "jimmy@mail.com", first_name: "Jimmy") + + project = insert(:project, title: "Code Corps") + plan = insert(:stripe_connect_plan, project: project) + subscription = insert( + :stripe_connect_subscription, + id_from_stripe: invoice_fixture.subscription, + stripe_connect_plan: plan, + user: user + ) + + invoice = insert( + :stripe_invoice, + id_from_stripe: invoice_fixture.id, + stripe_connect_subscription: subscription, + user: user + ) + + charge = insert( + :stripe_connect_charge, + amount: 500, + id_from_stripe: invoice.charge_id_from_stripe, + invoice_id_from_stripe: invoice.id_from_stripe, + user: user, + statement_descriptor: "Test descriptor" + ) + + insert(:donation_goal, project: project, current: true, description: "Test goal") + + %{substitution_data: data} = Receipt.build(charge, invoice_fixture) + + expected_keys = "receipt" |> CodeCorps.SparkPostHelpers.get_keys_used_by_template + assert data |> Map.keys == expected_keys + end + + test "builds correct transmission model" do + invoice_fixture = CodeCorps.StripeTesting.Helpers.load_fixture("invoice") + + user = insert(:user, email: "jimmy@mail.com", first_name: "Jimmy") + + project = insert(:project, title: "Code Corps") + plan = insert(:stripe_connect_plan, project: project) + subscription = insert( + :stripe_connect_subscription, + id_from_stripe: invoice_fixture.subscription, + stripe_connect_plan: plan, + user: user + ) + + invoice = insert( + :stripe_invoice, + id_from_stripe: invoice_fixture.id, + stripe_connect_subscription: subscription, + user: user + ) + + charge = insert( + :stripe_connect_charge, + amount: 500, + id_from_stripe: invoice.charge_id_from_stripe, + invoice_id_from_stripe: invoice.id_from_stripe, + user: user, + statement_descriptor: "Test descriptor" + ) + + insert(:donation_goal, project: project, current: true, description: "Test goal") + + %{substitution_data: data, recipients: [recipient]} = Receipt.build(charge, invoice_fixture) + + expected_model = %{ + charge_amount: "$5.00", + charge_statement_descriptor: "Test descriptor", + name: "Jimmy", + project_title: "Code Corps", + project_url: "http://localhost:4200/#{project.organization.slug}/#{project.slug}", + project_current_donation_goal_description: "Test goal", + subject: "Your monthly donation to Code Corps" + } + + assert data |> Map.take(expected_model |> Map.keys) == expected_model + + assert data.from_name == "Code Corps" + assert data.from_email == "team@codecorps.org" + assert data.high_five_image_url + + assert recipient.email == "jimmy@mail.com" + assert recipient.name == "Jimmy" + end + end +end diff --git a/test/lib/code_corps/stripe_service/events/connect_charge_succeeded_test.exs b/test/lib/code_corps/stripe_service/events/connect_charge_succeeded_test.exs index bc9917a2d..087e02b59 100644 --- a/test/lib/code_corps/stripe_service/events/connect_charge_succeeded_test.exs +++ b/test/lib/code_corps/stripe_service/events/connect_charge_succeeded_test.exs @@ -3,8 +3,6 @@ defmodule CodeCorps.StripeService.Events.ConnectChargeSucceededTest do use CodeCorps.StripeCase - use Bamboo.Test - alias CodeCorps.{ Project, Repo, StripeConnectCharge, StripeTesting } @@ -29,17 +27,14 @@ defmodule CodeCorps.StripeService.Events.ConnectChargeSucceededTest do user_id: account.id_from_stripe } - Bamboo.SentEmail.start_link() - assert { :ok, %StripeConnectCharge{} = charge, - %Bamboo.Email{} = email + %SparkPost.Transmission.Response{} } = ConnectChargeSucceeded.handle(event) # assert email was sent - - assert_delivered_email email + assert_received %SparkPost.Transmission{content: %{template_id: "receipt"}} # assert event was tracked by Segment From 80ac4fbd4388ae0ad6d7b0b87ff2373f80c33855 Mon Sep 17 00:00:00 2001 From: Nikola Begedin Date: Fri, 22 Dec 2017 09:14:00 +0100 Subject: [PATCH 05/20] Fix recipient data --- .../sparkpost/emails/forgot_password.ex | 5 ++-- lib/code_corps/sparkpost/emails/receipt.ex | 5 ++-- lib/code_corps/sparkpost/emails/recipient.ex | 27 +++++++++++++++++++ .../sparkpost/emails/forgot_password_test.exs | 4 +-- .../sparkpost/emails/receipt_test.exs | 4 +-- 5 files changed, 37 insertions(+), 8 deletions(-) create mode 100644 lib/code_corps/sparkpost/emails/recipient.ex diff --git a/lib/code_corps/sparkpost/emails/forgot_password.ex b/lib/code_corps/sparkpost/emails/forgot_password.ex index 765c32c37..a740160d6 100644 --- a/lib/code_corps/sparkpost/emails/forgot_password.ex +++ b/lib/code_corps/sparkpost/emails/forgot_password.ex @@ -1,13 +1,14 @@ defmodule CodeCorps.SparkPost.Emails.ForgotPassword do - alias CodeCorps.{User, WebClient} + alias SparkPost.{Content, Transmission} + alias CodeCorps.{SparkPost.Emails.Recipient, User, WebClient} @spec build(User.t, String.t) :: %Transmission{} def build(%User{} = user, token) do %Transmission{ content: %Content.TemplateRef{template_id: "forgot-password"}, options: %Transmission.Options{inline_css: true}, - recipients: [%{name: user.first_name, email: user.email}], + recipients: [user |> Recipient.build], substitution_data: %{ from_name: "Code Corps", from_email: "team@codecorps.org", diff --git a/lib/code_corps/sparkpost/emails/receipt.ex b/lib/code_corps/sparkpost/emails/receipt.ex index 39643add6..285b64680 100644 --- a/lib/code_corps/sparkpost/emails/receipt.ex +++ b/lib/code_corps/sparkpost/emails/receipt.ex @@ -1,15 +1,16 @@ defmodule CodeCorps.SparkPost.Emails.Receipt do + alias SparkPost.{Content, Transmission} alias CodeCorps.{ DonationGoal, Project, Repo, + SparkPost.Emails.Recipient, StripeConnectCharge, StripeConnectSubscription, User, WebClient } - alias SparkPost.{Content, Transmission} @high_five_image_urls [ "https://d3pgew4wbk2vb1.cloudfront.net/emails/images/emoji-1f64c-1f3fb@2x.png", @@ -28,7 +29,7 @@ defmodule CodeCorps.SparkPost.Emails.Receipt do %Transmission{ content: %Content.TemplateRef{template_id: "receipt"}, options: %Transmission.Options{inline_css: true}, - recipients: [%{name: user.first_name, email: user.email}], + recipients: [user |> Recipient.build], substitution_data: %{ charge_amount: charge.amount |> Money.new(:USD) |> Money.to_string(), charge_statement_descriptor: charge.statement_descriptor, diff --git a/lib/code_corps/sparkpost/emails/recipient.ex b/lib/code_corps/sparkpost/emails/recipient.ex new file mode 100644 index 000000000..c3f345717 --- /dev/null +++ b/lib/code_corps/sparkpost/emails/recipient.ex @@ -0,0 +1,27 @@ +defmodule CodeCorps.SparkPost.Emails.Recipient do + @moduledoc ~S""" + In charge of adapting `CodeCorps.User` data into SparkPost recipient data. + """ + + alias CodeCorps.User + + @doc ~S""" + From the provided user, builds a valid SparkPost recipient + + See https://developers.sparkpost.com/api/recipient-lists.html#header-address-attributes + + Though SparkPost specifies the address could also be a string type, containing + just the email, it's simpler to always treat it as a map, so that is what we + do. + """ + @spec build(User.t) :: map + def build(%User{} = user) do + %{address: user |> build_address()} + end + + @spec build_address(User.t) :: map + defp build_address(%User{first_name: nil, email: email}), do: %{email: email} + defp build_address(%User{first_name: name, email: email}) do + %{email: email, name: name} + end +end diff --git a/test/lib/code_corps/sparkpost/emails/forgot_password_test.exs b/test/lib/code_corps/sparkpost/emails/forgot_password_test.exs index d7b12a4de..c286186ce 100644 --- a/test/lib/code_corps/sparkpost/emails/forgot_password_test.exs +++ b/test/lib/code_corps/sparkpost/emails/forgot_password_test.exs @@ -24,8 +24,8 @@ defmodule CodeCorps.SparkPost.Emails.ForgotPasswordTest do assert data.from_email == "team@codecorps.org" assert data.link == "#{WebClient.url()}/password/reset?token=#{token}" - assert recipient.email == user.email - assert recipient.name == user.first_name + assert recipient.address.email == user.email + assert recipient.address.name == user.first_name end end end diff --git a/test/lib/code_corps/sparkpost/emails/receipt_test.exs b/test/lib/code_corps/sparkpost/emails/receipt_test.exs index 2e15343e8..75f8a27f7 100644 --- a/test/lib/code_corps/sparkpost/emails/receipt_test.exs +++ b/test/lib/code_corps/sparkpost/emails/receipt_test.exs @@ -92,8 +92,8 @@ defmodule CodeCorps.SparkPost.Emails.ReceiptTest do assert data.from_email == "team@codecorps.org" assert data.high_five_image_url - assert recipient.email == "jimmy@mail.com" - assert recipient.name == "Jimmy" + assert recipient.address.email == "jimmy@mail.com" + assert recipient.address.name == "Jimmy" end end end From 67887d4b324e873826b146d78bb05e863518ab49 Mon Sep 17 00:00:00 2001 From: Nikola Begedin Date: Fri, 22 Dec 2017 11:31:13 +0100 Subject: [PATCH 06/20] Switch message initiated by project email to Sparkpost --- .../message_initiated_by_project_email.ex | 38 ----------------- lib/code_corps/messages/emails.ex | 6 +-- lib/code_corps/sparkpost/emails/emails.ex | 6 +++ .../emails/message_initiated_by_project.ex | 38 +++++++++++++++++ lib/code_corps/sparkpost/sparkpost.ex | 1 + ...essage_initiated_by_project_email_test.exs | 25 ----------- .../lib/code_corps/messages/messages_test.exs | 11 +++-- .../message_initiated_by_project_test.exs | 41 +++++++++++++++++++ 8 files changed, 97 insertions(+), 69 deletions(-) delete mode 100644 lib/code_corps/emails/message_initiated_by_project_email.ex create mode 100644 lib/code_corps/sparkpost/emails/message_initiated_by_project.ex delete mode 100644 test/lib/code_corps/emails/message_initiated_by_project_email_test.exs create mode 100644 test/lib/code_corps/sparkpost/emails/message_initiated_by_project_test.exs diff --git a/lib/code_corps/emails/message_initiated_by_project_email.ex b/lib/code_corps/emails/message_initiated_by_project_email.ex deleted file mode 100644 index 9810912f0..000000000 --- a/lib/code_corps/emails/message_initiated_by_project_email.ex +++ /dev/null @@ -1,38 +0,0 @@ -defmodule CodeCorps.Emails.MessageInitiatedByProjectEmail do - import Bamboo.Email, only: [to: 2] - import Bamboo.PostmarkHelper - - alias CodeCorps.{ - Conversation, - Emails.BaseEmail, - Message, - Project, - User, - WebClient - } - - @spec create(User.t, String.t) :: Bamboo.Email.t - def create( - %Message{project: %Project{} = project}, - %Conversation{user: %User{} = user} = conversation) do - - BaseEmail.create - |> to(user.email) - |> template(template_id(), %{ - conversation_url: conversation |> conversation_url(), - name: user.first_name, - project_title: project.title, - subject: "You have a new message from #{project.title}" - }) - end - - @spec template_id :: String.t - defp template_id, do: Application.get_env(:code_corps, :postmark_message_initiated_by_project_template) - - @spec conversation_url(Conversation.t) :: String.t - defp conversation_url(%Conversation{id: id}) do - WebClient.url() - |> URI.merge("conversations/#{id}") - |> URI.to_string - end -end diff --git a/lib/code_corps/messages/emails.ex b/lib/code_corps/messages/emails.ex index 571d906cc..a93f4099b 100644 --- a/lib/code_corps/messages/emails.ex +++ b/lib/code_corps/messages/emails.ex @@ -7,7 +7,8 @@ defmodule CodeCorps.Messages.Emails do Emails, Mailer, Message, - Repo + Repo, + SparkPost } @message_preloads [:project, [conversations: :user]] @@ -24,8 +25,7 @@ defmodule CodeCorps.Messages.Emails do message |> Map.get(:conversations) - |> Enum.map(&Emails.MessageInitiatedByProjectEmail.create(message, &1)) - |> Enum.each(&Mailer.deliver_now/1) + |> Enum.each(&SparkPost.send_message_initiated_by_project_email(message, &1)) end @part_preloads [ diff --git a/lib/code_corps/sparkpost/emails/emails.ex b/lib/code_corps/sparkpost/emails/emails.ex index ea465d997..a2da0a0ef 100644 --- a/lib/code_corps/sparkpost/emails/emails.ex +++ b/lib/code_corps/sparkpost/emails/emails.ex @@ -5,6 +5,12 @@ defmodule CodeCorps.SparkPost.Emails do user |> Emails.ForgotPassword.build(token) |> API.send_transmission end + def send_message_initiated_by_project_email(message, conversation) do + message + |> Emails.MessageInitiatedByProject.build(conversation) + |> API.send_transmission + end + def send_receipt_email(charge, invoice) do case charge |> Emails.Receipt.build(invoice) do %SparkPost.Transmission{} = transmission -> diff --git a/lib/code_corps/sparkpost/emails/message_initiated_by_project.ex b/lib/code_corps/sparkpost/emails/message_initiated_by_project.ex new file mode 100644 index 000000000..5dd8983f7 --- /dev/null +++ b/lib/code_corps/sparkpost/emails/message_initiated_by_project.ex @@ -0,0 +1,38 @@ +defmodule CodeCorps.SparkPost.Emails.MessageInitiatedByProject do + alias SparkPost.{Content, Transmission} + alias CodeCorps.{ + Conversation, + Message, + Project, + SparkPost.Emails.Recipient, + User, + WebClient + } + + @spec build(User.t, String.t) :: %Transmission{} + def build( + %Message{project: %Project{} = project}, + %Conversation{user: %User{} = user} = conversation) do + + %Transmission{ + content: %Content.TemplateRef{template_id: "message-initiated-by-project"}, + options: %Transmission.Options{inline_css: true}, + recipients: [user |> Recipient.build], + substitution_data: %{ + conversation_url: conversation |> conversation_url(), + from_name: "Code Corps", + from_email: "team@codecorps.org", + name: user.first_name, + project_title: project.title, + subject: "You have a new message from #{project.title}" + } + } + end + + @spec conversation_url(Conversation.t) :: String.t + defp conversation_url(%Conversation{id: id}) do + WebClient.url() + |> URI.merge("conversations/#{id}") + |> URI.to_string + end +end diff --git a/lib/code_corps/sparkpost/sparkpost.ex b/lib/code_corps/sparkpost/sparkpost.ex index 6c2a3bcea..69e2c71e9 100644 --- a/lib/code_corps/sparkpost/sparkpost.ex +++ b/lib/code_corps/sparkpost/sparkpost.ex @@ -5,5 +5,6 @@ defmodule CodeCorps.SparkPost do defdelegate update_templates, to: Tasks defdelegate send_forgot_password_email(user, token), to: Emails + defdelegate send_message_initiated_by_project_email(message, conversation), to: Emails defdelegate send_receipt_email(charge, invoice), to: Emails end diff --git a/test/lib/code_corps/emails/message_initiated_by_project_email_test.exs b/test/lib/code_corps/emails/message_initiated_by_project_email_test.exs deleted file mode 100644 index f97d814fe..000000000 --- a/test/lib/code_corps/emails/message_initiated_by_project_email_test.exs +++ /dev/null @@ -1,25 +0,0 @@ -defmodule CodeCorps.Emails.MessageInitiatedByProjectEmailTest do - use CodeCorps.DbAccessCase - use Bamboo.Test - - alias CodeCorps.Emails.MessageInitiatedByProjectEmail - - test "email works" do - %{message: message} = conversation = - insert(:conversation) - |> Repo.preload([[message: :project], :user]) - - email = MessageInitiatedByProjectEmail.create(message, conversation) - assert email.from == "Code Corps" - assert email.to == conversation.user.email - - template_model = email.private.template_model - - assert template_model == %{ - conversation_url: "http://localhost:4200/conversations/#{conversation.id}", - name: conversation.user.first_name, - project_title: message.project.title, - subject: "You have a new message from #{message.project.title}" - } - end -end diff --git a/test/lib/code_corps/messages/messages_test.exs b/test/lib/code_corps/messages/messages_test.exs index 98a051640..a6fef8fe0 100644 --- a/test/lib/code_corps/messages/messages_test.exs +++ b/test/lib/code_corps/messages/messages_test.exs @@ -7,7 +7,10 @@ defmodule CodeCorps.MessagesTest do import Ecto.Query, only: [where: 2] - alias CodeCorps.{Conversation, ConversationPart, Emails, Message, Messages} + alias CodeCorps.{ + Conversation, ConversationPart, Emails, Message, Messages, + SparkPost.Emails.MessageInitiatedByProject + } alias Ecto.Changeset defp get_and_sort_ids(records) do @@ -456,8 +459,10 @@ defmodule CodeCorps.MessagesTest do %{conversations: [conversation_1, conversation_2]} = message = message |> Repo.preload([:project, [conversations: :user]]) - assert_delivered_email Emails.MessageInitiatedByProjectEmail.create(message, conversation_1) - assert_delivered_email Emails.MessageInitiatedByProjectEmail.create(message, conversation_2) + email_1 = MessageInitiatedByProject.build(message, conversation_1) + assert_received ^email_1 + email_2 = MessageInitiatedByProject.build(message, conversation_2) + assert_received ^email_2 end end end diff --git a/test/lib/code_corps/sparkpost/emails/message_initiated_by_project_test.exs b/test/lib/code_corps/sparkpost/emails/message_initiated_by_project_test.exs new file mode 100644 index 000000000..4b461676a --- /dev/null +++ b/test/lib/code_corps/sparkpost/emails/message_initiated_by_project_test.exs @@ -0,0 +1,41 @@ +defmodule CodeCorps.SparkPost.Emails.MessageInitiatedByProjectTest do + use CodeCorps.DbAccessCase + + alias CodeCorps.SparkPost.Emails.MessageInitiatedByProject + + describe "build/2" do + test "provides substitution data for all keys used by template" do + %{message: message} = conversation = + insert(:conversation) + |> Repo.preload([[message: :project], :user]) + + %{substitution_data: data} = + MessageInitiatedByProject.build(message, conversation) + + expected_keys = + "message-initiated-by-project" + |> CodeCorps.SparkPostHelpers.get_keys_used_by_template + assert data |> Map.keys == expected_keys + end + + test "builds correct transmission model" do + %{message: message, user: user} = conversation = + insert(:conversation) + |> Repo.preload([[message: :project], :user]) + + %{substitution_data: data, recipients: [recipient]} = + MessageInitiatedByProject.build(message, conversation) + + assert data.from_name == "Code Corps" + assert data.from_email == "team@codecorps.org" + + assert data.conversation_url == "http://localhost:4200/conversations/#{conversation.id}" + assert data.name == conversation.user.first_name + assert data.project_title == message.project.title + assert data.subject == "You have a new message from #{message.project.title}" + + assert recipient.address.email == user.email + assert recipient.address.name == user.first_name + end + end +end From d16239962bae811057105ac5fe6f34eb3effafc9 Mon Sep 17 00:00:00 2001 From: Nikola Begedin Date: Fri, 22 Dec 2017 11:58:30 +0100 Subject: [PATCH 07/20] Switch organization invite email to SparkPost --- .../emails/organization_invite_email.ex | 39 ------------------ lib/code_corps/sparkpost/emails/emails.ex | 4 ++ .../sparkpost/emails/organization_invite.ex | 34 ++++++++++++++++ lib/code_corps/sparkpost/emails/recipient.ex | 17 ++++++-- lib/code_corps/sparkpost/sparkpost.ex | 1 + .../organization_invite_controller.ex | 10 +---- .../emails/organization_invite_email_test.exs | 26 ------------ .../emails/organization_invite_test.exs | 40 +++++++++++++++++++ .../organization_invite_controller_test.exs | 6 +-- 9 files changed, 96 insertions(+), 81 deletions(-) delete mode 100644 lib/code_corps/emails/organization_invite_email.ex create mode 100644 lib/code_corps/sparkpost/emails/organization_invite.ex delete mode 100644 test/lib/code_corps/emails/organization_invite_email_test.exs create mode 100644 test/lib/code_corps/sparkpost/emails/organization_invite_test.exs diff --git a/lib/code_corps/emails/organization_invite_email.ex b/lib/code_corps/emails/organization_invite_email.ex deleted file mode 100644 index 123d37e65..000000000 --- a/lib/code_corps/emails/organization_invite_email.ex +++ /dev/null @@ -1,39 +0,0 @@ -defmodule CodeCorps.Emails.OrganizationInviteEmail do - import Bamboo.Email, only: [to: 2] - import Bamboo.PostmarkHelper - - alias CodeCorps.{Emails.BaseEmail, OrganizationInvite, WebClient} - - @spec create(OrganizationInvite.t) :: Bamboo.Email.t - def create(%OrganizationInvite{} = invite) do - BaseEmail.create - |> to(invite.email) - |> template(template_id(), build_model(invite)) - end - - @spec build_model(OrganizationInvite.t) :: map - defp build_model(%OrganizationInvite{} = invite) do - %{ - organization_name: invite.organization_name, - invite_url: invite_url(invite.code, invite.organization_name), - subject: "Create your first project on Code Corps" - } - end - - @spec invite_url(String.t, String.t) :: String.t - defp invite_url(code, organization_name) do - query_params = set_params(code, organization_name) - WebClient.url() - |> URI.merge("/organizations/new" <> "?" <> query_params) - |> URI.to_string - end - - @spec set_params(String.t, String.t) :: binary - defp set_params(code, organization_name) do - %{code: code, organization_name: organization_name} - |> URI.encode_query - end - - @spec template_id :: String.t - defp template_id, do: Application.get_env(:code_corps, :postmark_organization_invite_email_template) -end diff --git a/lib/code_corps/sparkpost/emails/emails.ex b/lib/code_corps/sparkpost/emails/emails.ex index a2da0a0ef..16ea9d54f 100644 --- a/lib/code_corps/sparkpost/emails/emails.ex +++ b/lib/code_corps/sparkpost/emails/emails.ex @@ -11,6 +11,10 @@ defmodule CodeCorps.SparkPost.Emails do |> API.send_transmission end + def send_organization_invite_email(invite) do + invite |> Emails.OrganizationInvite.build |> API.send_transmission + end + def send_receipt_email(charge, invoice) do case charge |> Emails.Receipt.build(invoice) do %SparkPost.Transmission{} = transmission -> diff --git a/lib/code_corps/sparkpost/emails/organization_invite.ex b/lib/code_corps/sparkpost/emails/organization_invite.ex new file mode 100644 index 000000000..096200c5a --- /dev/null +++ b/lib/code_corps/sparkpost/emails/organization_invite.ex @@ -0,0 +1,34 @@ +defmodule CodeCorps.SparkPost.Emails.OrganizationInvite do + alias SparkPost.{Content, Transmission} + alias CodeCorps.{OrganizationInvite, SparkPost.Emails.Recipient, WebClient} + + @spec build(OrganizationInvite.t) :: %Transmission{} + def build(%OrganizationInvite{} = invite) do + %Transmission{ + content: %Content.TemplateRef{template_id: "organization-invite"}, + options: %Transmission.Options{inline_css: true}, + recipients: [invite |> Recipient.build], + substitution_data: %{ + from_name: "Code Corps", + from_email: "team@codecorps.org", + organization_name: invite.organization_name, + invite_url: invite_url(invite.code, invite.organization_name), + subject: "Create your first project on Code Corps" + } + } + end + + @spec invite_url(String.t, String.t) :: String.t + defp invite_url(code, organization_name) do + query_params = set_params(code, organization_name) + WebClient.url() + |> URI.merge("/organizations/new" <> "?" <> query_params) + |> URI.to_string + end + + @spec set_params(String.t, String.t) :: binary + defp set_params(code, organization_name) do + %{code: code, organization_name: organization_name} + |> URI.encode_query + end +end diff --git a/lib/code_corps/sparkpost/emails/recipient.ex b/lib/code_corps/sparkpost/emails/recipient.ex index c3f345717..0df980317 100644 --- a/lib/code_corps/sparkpost/emails/recipient.ex +++ b/lib/code_corps/sparkpost/emails/recipient.ex @@ -3,10 +3,10 @@ defmodule CodeCorps.SparkPost.Emails.Recipient do In charge of adapting `CodeCorps.User` data into SparkPost recipient data. """ - alias CodeCorps.User + alias CodeCorps.{OrganizationInvite, User} @doc ~S""" - From the provided user, builds a valid SparkPost recipient + From the provided record, builds a valid SparkPost recipient See https://developers.sparkpost.com/api/recipient-lists.html#header-address-attributes @@ -14,14 +14,23 @@ defmodule CodeCorps.SparkPost.Emails.Recipient do just the email, it's simpler to always treat it as a map, so that is what we do. """ - @spec build(User.t) :: map + @spec build(OrganizationInvite.t | User.t) :: map def build(%User{} = user) do %{address: user |> build_address()} end + def build(%OrganizationInvite{} = invite) do + %{address: invite |> build_address()} + end - @spec build_address(User.t) :: map + @spec build_address(OrganizationInvite.t | User.t) :: map defp build_address(%User{first_name: nil, email: email}), do: %{email: email} defp build_address(%User{first_name: name, email: email}) do %{email: email, name: name} end + defp build_address(%OrganizationInvite{organization_name: nil, email: email}) do + %{email: email} + end + defp build_address(%OrganizationInvite{organization_name: name, email: email}) do + %{email: email, name: name} + end end diff --git a/lib/code_corps/sparkpost/sparkpost.ex b/lib/code_corps/sparkpost/sparkpost.ex index 69e2c71e9..ac31245b0 100644 --- a/lib/code_corps/sparkpost/sparkpost.ex +++ b/lib/code_corps/sparkpost/sparkpost.ex @@ -6,5 +6,6 @@ defmodule CodeCorps.SparkPost do defdelegate send_forgot_password_email(user, token), to: Emails defdelegate send_message_initiated_by_project_email(message, conversation), to: Emails + defdelegate send_organization_invite_email(invite), to: Emails defdelegate send_receipt_email(charge, invoice), to: Emails end diff --git a/lib/code_corps_web/controllers/organization_invite_controller.ex b/lib/code_corps_web/controllers/organization_invite_controller.ex index 503002d4e..e82985b03 100644 --- a/lib/code_corps_web/controllers/organization_invite_controller.ex +++ b/lib/code_corps_web/controllers/organization_invite_controller.ex @@ -2,7 +2,7 @@ defmodule CodeCorpsWeb.OrganizationInviteController do @moduledoc false use CodeCorpsWeb, :controller - alias CodeCorps.{Emails, Helpers.Query, Mailer, OrganizationInvite, User} + alias CodeCorps.{Helpers.Query, OrganizationInvite, SparkPost, User} action_fallback CodeCorpsWeb.FallbackController plug CodeCorpsWeb.Plug.DataToAttributes @@ -28,7 +28,7 @@ defmodule CodeCorpsWeb.OrganizationInviteController do {:ok, :authorized} <- current_user |> Policy.authorize(:create, %OrganizationInvite{}, params), {:ok, %OrganizationInvite{} = organization_invite} <- %OrganizationInvite{} |> OrganizationInvite.create_changeset(params) |> Repo.insert do - send_email(organization_invite) + organization_invite |> SparkPost.send_organization_invite_email conn |> put_status(:created) @@ -45,10 +45,4 @@ defmodule CodeCorpsWeb.OrganizationInviteController do conn |> render("show.json-api", data: organization_invite) end end - - defp send_email(organization_invite) do - organization_invite - |> Emails.OrganizationInviteEmail.create() - |> Mailer.deliver_later() - end end diff --git a/test/lib/code_corps/emails/organization_invite_email_test.exs b/test/lib/code_corps/emails/organization_invite_email_test.exs deleted file mode 100644 index 8d2795bf8..000000000 --- a/test/lib/code_corps/emails/organization_invite_email_test.exs +++ /dev/null @@ -1,26 +0,0 @@ -defmodule CodeCorps.Emails.OrganizationInviteEmailTest do - use CodeCorps.ModelCase - use Bamboo.Test - - alias CodeCorps.{Emails.OrganizationInviteEmail, WebClient} - - test "organization email invite works" do - invite = insert(:organization_invite) - email = OrganizationInviteEmail.create(invite) - - assert email.from == "Code Corps" - assert email.to == invite.email - - template_model = email.private.template_model - params = - %{code: invite.code, organization_name: invite.organization_name} - |> URI.encode_query - invite_url = "#{WebClient.url()}/organizations/new?#{params}" - - assert template_model == %{ - invite_url: invite_url, - organization_name: invite.organization_name, - subject: "Create your first project on Code Corps" - } - end - end diff --git a/test/lib/code_corps/sparkpost/emails/organization_invite_test.exs b/test/lib/code_corps/sparkpost/emails/organization_invite_test.exs new file mode 100644 index 000000000..840abd3f9 --- /dev/null +++ b/test/lib/code_corps/sparkpost/emails/organization_invite_test.exs @@ -0,0 +1,40 @@ +defmodule CodeCorps.SparkPost.Emails.OrganizationInviteTest do + use CodeCorps.DbAccessCase + + alias CodeCorps.{SparkPost.Emails.OrganizationInvite, WebClient} + + describe "build/1" do + test "provides substitution data for all keys used by template" do + invite = insert(:organization_invite) + + %{substitution_data: data} = OrganizationInvite.build(invite) + + expected_keys = + "organization-invite" + |> CodeCorps.SparkPostHelpers.get_keys_used_by_template + assert data |> Map.keys == expected_keys + end + + test "builds correct transmission model" do + invite = insert(:organization_invite) + + %{substitution_data: data, recipients: [recipient]} = + OrganizationInvite.build(invite) + + assert data.from_name == "Code Corps" + assert data.from_email == "team@codecorps.org" + + assert data.organization_name == invite.organization_name + assert data.subject == "Create your first project on Code Corps" + + params = + %{code: invite.code, organization_name: invite.organization_name} + |> URI.encode_query + + assert data.invite_url == "#{WebClient.url()}/organizations/new?#{params}" + + assert recipient.address.email == invite.email + assert recipient.address.name == invite.organization_name + end + end +end diff --git a/test/lib/code_corps_web/controllers/organization_invite_controller_test.exs b/test/lib/code_corps_web/controllers/organization_invite_controller_test.exs index d0d1d2a31..a2f2d745f 100644 --- a/test/lib/code_corps_web/controllers/organization_invite_controller_test.exs +++ b/test/lib/code_corps_web/controllers/organization_invite_controller_test.exs @@ -1,6 +1,5 @@ defmodule CodeCorpsWeb.OrganizationInviteControllerTest do use CodeCorpsWeb.ApiCase, resource_name: :organization_invite - use Bamboo.Test @valid_attrs %{email: "code@corps.com", organization_name: "Code Corps"} @invalid_attrs %{email: "code", organization_name: ""} @@ -38,11 +37,10 @@ defmodule CodeCorpsWeb.OrganizationInviteControllerTest do organization_invite_email = CodeCorps.OrganizationInvite - |> first() |> Repo.one() - |> CodeCorps.Emails.OrganizationInviteEmail.create() + |> CodeCorps.SparkPost.Emails.OrganizationInvite.build() - assert_delivered_email organization_invite_email + assert_received ^organization_invite_email end @tag authenticated: :admin From 2ac71ffaf87fbb18948e8730396b3fc85a677871 Mon Sep 17 00:00:00 2001 From: Nikola Begedin Date: Fri, 22 Dec 2017 12:14:53 +0100 Subject: [PATCH 08/20] Switch reply to conversation email to SparkPost --- .../emails/reply_to_conversation_email.ex | 51 --------------- lib/code_corps/messages/emails.ex | 5 +- lib/code_corps/sparkpost/emails/emails.ex | 4 ++ .../sparkpost/emails/reply_to_conversation.ex | 55 ++++++++++++++++ lib/code_corps/sparkpost/sparkpost.ex | 1 + .../reply_to_conversation_email_test.exs | 37 ----------- .../lib/code_corps/messages/messages_test.exs | 29 ++++++--- .../emails/reply_to_conversation_test.exs | 62 +++++++++++++++++++ 8 files changed, 142 insertions(+), 102 deletions(-) delete mode 100644 lib/code_corps/emails/reply_to_conversation_email.ex create mode 100644 lib/code_corps/sparkpost/emails/reply_to_conversation.ex delete mode 100644 test/lib/code_corps/emails/reply_to_conversation_email_test.exs create mode 100644 test/lib/code_corps/sparkpost/emails/reply_to_conversation_test.exs diff --git a/lib/code_corps/emails/reply_to_conversation_email.ex b/lib/code_corps/emails/reply_to_conversation_email.ex deleted file mode 100644 index dfb781ff4..000000000 --- a/lib/code_corps/emails/reply_to_conversation_email.ex +++ /dev/null @@ -1,51 +0,0 @@ -defmodule CodeCorps.Emails.ReplyToConversationEmail do - import Bamboo.Email, only: [to: 2] - import Bamboo.PostmarkHelper - - alias CodeCorps.{ - Conversation, - ConversationPart, - Emails.BaseEmail, - Message, - Organization, - Project, - User, - WebClient - } - - @spec create(ConversationPart.t, User.t) :: Bamboo.Email.t - def create( - %ConversationPart{ - author: %User{} = author, - conversation: %Conversation{ - message: %Message{ - project: %Project{} = project - } - } = conversation - }, - %User{} = user) do - - BaseEmail.create - |> to(user.email) - |> template(template_id(), %{ - author_name: author.first_name, - conversation_url: project |> conversation_url(conversation), - name: user.first_name, - project_title: project.title, - subject: "#{author.first_name} replied to your conversation in #{project.title}" - }) - end - - @spec template_id :: String.t - defp template_id, do: Application.get_env(:code_corps, :postmark_reply_to_conversation_template) - - @spec conversation_url(Project.t, Conversation.t) :: String.t - defp conversation_url( - %Project{organization: %Organization{slug: slug}, slug: project_slug}, - %Conversation{id: id}) do - - WebClient.url() - |> URI.merge("#{slug}/#{project_slug}/conversations/#{id}") - |> URI.to_string - end -end diff --git a/lib/code_corps/messages/emails.ex b/lib/code_corps/messages/emails.ex index a93f4099b..f4f3870aa 100644 --- a/lib/code_corps/messages/emails.ex +++ b/lib/code_corps/messages/emails.ex @@ -4,8 +4,6 @@ defmodule CodeCorps.Messages.Emails do """ alias CodeCorps.{ ConversationPart, - Emails, - Mailer, Message, Repo, SparkPost @@ -54,8 +52,7 @@ defmodule CodeCorps.Messages.Emails do defp send_reply_to_conversation_emails(%ConversationPart{} = part) do part |> get_conversation_participants() - |> Enum.map(&Emails.ReplyToConversationEmail.create(part, &1)) - |> Enum.each(&Mailer.deliver_now/1) + |> Enum.each(&SparkPost.send_reply_to_conversation_email(part, &1)) end @spec get_conversation_participants(ConversationPart.t) :: list(User.t) diff --git a/lib/code_corps/sparkpost/emails/emails.ex b/lib/code_corps/sparkpost/emails/emails.ex index 16ea9d54f..975e2c109 100644 --- a/lib/code_corps/sparkpost/emails/emails.ex +++ b/lib/code_corps/sparkpost/emails/emails.ex @@ -22,4 +22,8 @@ defmodule CodeCorps.SparkPost.Emails do build_failure -> build_failure end end + + def send_reply_to_conversation_email(part, user) do + part |> Emails.ReplyToConversation.build(user) |> API.send_transmission + end end diff --git a/lib/code_corps/sparkpost/emails/reply_to_conversation.ex b/lib/code_corps/sparkpost/emails/reply_to_conversation.ex new file mode 100644 index 000000000..380a90ac8 --- /dev/null +++ b/lib/code_corps/sparkpost/emails/reply_to_conversation.ex @@ -0,0 +1,55 @@ +defmodule CodeCorps.SparkPost.Emails.ReplyToConversation do + alias SparkPost.{Content, Transmission} + alias CodeCorps.{ + Conversation, + ConversationPart, + Message, + Organization, + Project, + SparkPost.Emails.Recipient, + User, + WebClient + } + + @spec build(ConversationPart.t, User.t) :: %Transmission{} + def build( + %ConversationPart{ + author: %User{} = author, + conversation: %Conversation{ + message: %Message{ + project: %Project{} = project + } + } = conversation + }, + %User{} = user) do + + %Transmission{ + content: %Content.TemplateRef{template_id: "reply-to-conversation"}, + options: %Transmission.Options{inline_css: true}, + recipients: [user |> Recipient.build], + substitution_data: %{ + author_name: author.first_name, + from_name: "Code Corps", + from_email: "team@codecorps.org", + conversation_url: project |> conversation_url(conversation), + name: user |> get_name(), + project_title: project.title, + subject: "#{author.first_name} replied to your conversation in #{project.title}" + } + } + end + + @spec conversation_url(Project.t, Conversation.t) :: String.t + defp conversation_url( + %Project{organization: %Organization{slug: slug}, slug: project_slug}, + %Conversation{id: id}) do + + WebClient.url() + |> URI.merge("#{slug}/#{project_slug}/conversations/#{id}") + |> URI.to_string + end + + @spec get_name(User.t) :: String.t + defp get_name(%User{first_name: nil}), do: "there" + defp get_name(%User{first_name: name}), do: name +end diff --git a/lib/code_corps/sparkpost/sparkpost.ex b/lib/code_corps/sparkpost/sparkpost.ex index ac31245b0..f64bfc240 100644 --- a/lib/code_corps/sparkpost/sparkpost.ex +++ b/lib/code_corps/sparkpost/sparkpost.ex @@ -8,4 +8,5 @@ defmodule CodeCorps.SparkPost do defdelegate send_message_initiated_by_project_email(message, conversation), to: Emails defdelegate send_organization_invite_email(invite), to: Emails defdelegate send_receipt_email(charge, invoice), to: Emails + defdelegate send_reply_to_conversation_email(part, user), to: Emails end diff --git a/test/lib/code_corps/emails/reply_to_conversation_email_test.exs b/test/lib/code_corps/emails/reply_to_conversation_email_test.exs deleted file mode 100644 index 9fb91be9f..000000000 --- a/test/lib/code_corps/emails/reply_to_conversation_email_test.exs +++ /dev/null @@ -1,37 +0,0 @@ -defmodule CodeCorps.Emails.ReplyToConversationEmailTest do - use CodeCorps.DbAccessCase - use Bamboo.Test - - alias CodeCorps.Emails.ReplyToConversationEmail - - test "email works" do - message = insert(:message) - - preloads = [:author, conversation: [message: [[project: :organization]]]] - - conversation = insert(:conversation, message: message) - - conversation_part = - :conversation_part - |> insert(conversation: conversation) - |> Repo.preload(preloads) - - %{project: %{organization: %{slug: slug}, slug: project_slug} = project} = message - - user = insert(:user) - - email = ReplyToConversationEmail.create(conversation_part, user) - assert email.from == "Code Corps" - assert email.to == user.email - - template_model = email.private.template_model - - assert template_model == %{ - author_name: conversation_part.author.first_name, - conversation_url: "http://localhost:4200/#{slug}/#{project_slug}/conversations/#{conversation.id}", - name: user.first_name, - project_title: project.title, - subject: "#{conversation_part.author.first_name} replied to your conversation in #{project.title}" - } - end -end diff --git a/test/lib/code_corps/messages/messages_test.exs b/test/lib/code_corps/messages/messages_test.exs index a6fef8fe0..edf499742 100644 --- a/test/lib/code_corps/messages/messages_test.exs +++ b/test/lib/code_corps/messages/messages_test.exs @@ -8,8 +8,9 @@ defmodule CodeCorps.MessagesTest do import Ecto.Query, only: [where: 2] alias CodeCorps.{ - Conversation, ConversationPart, Emails, Message, Messages, - SparkPost.Emails.MessageInitiatedByProject + Conversation, ConversationPart, Message, Messages, + SparkPost.Emails.MessageInitiatedByProject, + SparkPost.Emails.ReplyToConversation } alias Ecto.Changeset @@ -302,10 +303,14 @@ defmodule CodeCorps.MessagesTest do part = part |> Repo.preload([:author, conversation: [message: [[project: :organization]]]]) - refute_delivered_email Emails.ReplyToConversationEmail.create(part, part_author) - assert_delivered_email Emails.ReplyToConversationEmail.create(part, target_user) - assert_delivered_email Emails.ReplyToConversationEmail.create(part, message_author) - assert_delivered_email Emails.ReplyToConversationEmail.create(part, other_participant) + part_author_email = ReplyToConversation.build(part, part_author) + target_user_email = ReplyToConversation.build(part, target_user) + message_author_email = ReplyToConversation.build(part, message_author) + other_participant_email = ReplyToConversation.build(part, other_participant) + refute_received ^part_author_email + assert_received ^target_user_email + assert_received ^message_author_email + assert_received ^other_participant_email end test "when replied by conversation user, sends appropriate email to other participants" do @@ -324,10 +329,14 @@ defmodule CodeCorps.MessagesTest do part = part |> Repo.preload([:author, conversation: [message: [[project: :organization]]]]) - refute_delivered_email Emails.ReplyToConversationEmail.create(part, part_author) - assert_delivered_email Emails.ReplyToConversationEmail.create(part, target_user) - assert_delivered_email Emails.ReplyToConversationEmail.create(part, message_author) - assert_delivered_email Emails.ReplyToConversationEmail.create(part, other_participant) + part_author_email = ReplyToConversation.build(part, part_author) + target_user_email = ReplyToConversation.build(part, target_user) + message_author_email = ReplyToConversation.build(part, message_author) + other_participant_email = ReplyToConversation.build(part, other_participant) + refute_received ^part_author_email + assert_received ^target_user_email + assert_received ^message_author_email + assert_received ^other_participant_email end end diff --git a/test/lib/code_corps/sparkpost/emails/reply_to_conversation_test.exs b/test/lib/code_corps/sparkpost/emails/reply_to_conversation_test.exs new file mode 100644 index 000000000..73e0f54f4 --- /dev/null +++ b/test/lib/code_corps/sparkpost/emails/reply_to_conversation_test.exs @@ -0,0 +1,62 @@ +defmodule CodeCorps.SparkPost.Emails.ReplyToConversationTest do + use CodeCorps.DbAccessCase + + alias CodeCorps.{SparkPost.Emails.ReplyToConversation, WebClient} + + describe "build/1" do + test "provides substitution data for all keys used by template" do + message = insert(:message) + + preloads = [:author, conversation: [message: [[project: :organization]]]] + + conversation = insert(:conversation, message: message) + + conversation_part = + :conversation_part + |> insert(conversation: conversation) + |> Repo.preload(preloads) + + user = insert(:user) + + %{substitution_data: data} = + ReplyToConversation.build(conversation_part, user) + + expected_keys = + "reply-to-conversation" + |> CodeCorps.SparkPostHelpers.get_keys_used_by_template + assert data |> Map.keys == expected_keys + end + + test "builds correct transmission model" do + message = insert(:message) + + preloads = [:author, conversation: [message: [[project: :organization]]]] + + conversation = insert(:conversation, message: message) + + conversation_part = + :conversation_part + |> insert(conversation: conversation) + |> Repo.preload(preloads) + + %{project: %{organization: %{slug: slug}, slug: project_slug} = project} = message + + user = insert(:user) + + %{substitution_data: data, recipients: [recipient]} = + ReplyToConversation.build(conversation_part, user) + + assert data.from_name == "Code Corps" + assert data.from_email == "team@codecorps.org" + + assert data.author_name == conversation_part.author.first_name + assert data.conversation_url == "#{WebClient.url()}/#{slug}/#{project_slug}/conversations/#{conversation.id}" + assert data.name == user.first_name + assert data.project_title == project.title + assert data.subject == "#{conversation_part.author.first_name} replied to your conversation in #{project.title}" + + assert recipient.address.email == user.email + assert recipient.address.name == user.first_name + end + end +end From 4ecae2e8ed9220e4760c23b96b8bc83823d9ece7 Mon Sep 17 00:00:00 2001 From: Nikola Begedin Date: Fri, 22 Dec 2017 12:30:15 +0100 Subject: [PATCH 09/20] Switch project approval request email to SparkPost --- .../emails/project_approval_request_email.ex | 63 ------------------- lib/code_corps/projects/projects.ex | 7 +-- lib/code_corps/sparkpost/emails/emails.ex | 4 ++ .../emails/project_approval_request.ex | 56 +++++++++++++++++ lib/code_corps/sparkpost/sparkpost.ex | 1 + .../project_approval_request_email_test.exs | 29 --------- .../emails/project_approval_request_test.exs | 43 +++++++++++++ .../controllers/project_controller_test.exs | 4 +- 8 files changed, 109 insertions(+), 98 deletions(-) delete mode 100644 lib/code_corps/emails/project_approval_request_email.ex create mode 100644 lib/code_corps/sparkpost/emails/project_approval_request.ex delete mode 100644 test/lib/code_corps/emails/project_approval_request_email_test.exs create mode 100644 test/lib/code_corps/sparkpost/emails/project_approval_request_test.exs diff --git a/lib/code_corps/emails/project_approval_request_email.ex b/lib/code_corps/emails/project_approval_request_email.ex deleted file mode 100644 index d44502369..000000000 --- a/lib/code_corps/emails/project_approval_request_email.ex +++ /dev/null @@ -1,63 +0,0 @@ -defmodule CodeCorps.Emails.ProjectApprovalRequestEmail do - import Bamboo.Email, only: [to: 2] - import Bamboo.PostmarkHelper - import Ecto.Query, only: [where: 3] - - alias CodeCorps.{Project, Repo, User, WebClient} - alias CodeCorps.Emails.BaseEmail - alias CodeCorps.Presenters.ImagePresenter - - @spec create(Project.t) :: Bamboo.Email.t - def create(%Project{} = project) do - BaseEmail.create - |> to(get_site_admins_emails()) - |> template(template_id(), build_model(project)) - end - - @spec build_model(Project.t) :: map - defp build_model(%Project{} = project) do - %{ - admin_project_show_url: project |> admin_url(), - project_description: project.description, - project_logo_url: ImagePresenter.large(project), - project_title: project.title, - project_url: project |> preload() |> project_url(), - subject: "#{project.title} is asking to be approved" - } - end - - @spec preload(Project.t) :: Project.t - defp preload(%Project{} = project), do: project |> Repo.preload(:organization) - - @spec admin_url(Project.t) :: String.t - defp admin_url(project) do - WebClient.url() - |> URI.merge("/admin/projects/" <> Integer.to_string(project.id)) - |> URI.to_string() - end - - @spec project_url(Project.t) :: String.t - defp project_url(project) do - WebClient.url() - |> URI.merge(project.organization.slug <> "/" <> project.slug) - |> URI.to_string() - end - - @spec template_id :: String.t - defp template_id, do: Application.get_env(:code_corps, :postmark_project_approval_request_template) - - @spec get_site_admins_emails() :: list(String.t) - defp get_site_admins_emails() do - get_site_admins() |> Enum.map(&extract_email/1) - end - - @spec extract_email(User.t) :: String.t - defp extract_email(%User{email: email}), do: email - - @spec get_site_admins() :: list(User.t) - defp get_site_admins() do - User - |> where([object], object.admin == true) - |> Repo.all() - end -end diff --git a/lib/code_corps/projects/projects.ex b/lib/code_corps/projects/projects.ex index 9b1917bbb..fb98dfbb5 100644 --- a/lib/code_corps/projects/projects.ex +++ b/lib/code_corps/projects/projects.ex @@ -6,7 +6,7 @@ defmodule CodeCorps.Projects do import CodeCorpsWeb.ProjectController, only: [preload: 1] alias CodeCorps.{ - Analytics.SegmentTracker, Emails, Mailer, Project, Repo, User + Analytics.SegmentTracker, Emails, Mailer, Project, Repo, SparkPost, User } alias Ecto.Changeset @@ -81,12 +81,11 @@ defmodule CodeCorps.Projects do end defp maybe_send_approval_request_email(%Project{}, %Project{}), do: :nothing - @spec send_approval_request_email(Project.t) :: Bamboo.Email.t + @spec send_approval_request_email(Project.t) :: tuple defp send_approval_request_email(project) do project |> preload() - |> Emails.ProjectApprovalRequestEmail.create() - |> Mailer.deliver_now() + |> SparkPost.send_project_approval_request_email() end @spec maybe_send_approved_email(Project.t, Project.t) :: any diff --git a/lib/code_corps/sparkpost/emails/emails.ex b/lib/code_corps/sparkpost/emails/emails.ex index 975e2c109..8ef988945 100644 --- a/lib/code_corps/sparkpost/emails/emails.ex +++ b/lib/code_corps/sparkpost/emails/emails.ex @@ -15,6 +15,10 @@ defmodule CodeCorps.SparkPost.Emails do invite |> Emails.OrganizationInvite.build |> API.send_transmission end + def send_project_approval_request_email(project) do + project |> Emails.ProjectApprovalRequest.build |> API.send_transmission + end + def send_receipt_email(charge, invoice) do case charge |> Emails.Receipt.build(invoice) do %SparkPost.Transmission{} = transmission -> diff --git a/lib/code_corps/sparkpost/emails/project_approval_request.ex b/lib/code_corps/sparkpost/emails/project_approval_request.ex new file mode 100644 index 000000000..fcc04a4ca --- /dev/null +++ b/lib/code_corps/sparkpost/emails/project_approval_request.ex @@ -0,0 +1,56 @@ +defmodule CodeCorps.SparkPost.Emails.ProjectApprovalRequest do + import Ecto.Query, only: [where: 3] + + alias SparkPost.{Content, Transmission} + alias CodeCorps.{ + Presenters.ImagePresenter, + Project, + Repo, + SparkPost.Emails.Recipient, + User, + WebClient + } + + @spec build(Project.t) :: %Transmission{} + def build(%Project{} = project) do + %Transmission{ + content: %Content.TemplateRef{template_id: "project-approval-request"}, + options: %Transmission.Options{inline_css: true}, + recipients: get_site_admins() |> Enum.map(&Recipient.build/1), + substitution_data: %{ + admin_project_show_url: project |> admin_url(), + from_name: "Code Corps", + from_email: "team@codecorps.org", + project_description: project.description, + project_logo_url: ImagePresenter.large(project), + project_title: project.title, + project_url: project |> preload() |> project_url(), + subject: "#{project.title} is asking to be approved" + } + } + end + + @spec preload(Project.t) :: Project.t + defp preload(%Project{} = project), do: project |> Repo.preload(:organization) + + @spec admin_url(Project.t) :: String.t + defp admin_url(project) do + WebClient.url() + |> URI.merge("/admin/projects/" <> Integer.to_string(project.id)) + |> URI.to_string() + end + + @spec project_url(Project.t) :: String.t + defp project_url(project) do + WebClient.url() + |> URI.merge(project.organization.slug <> "/" <> project.slug) + |> URI.to_string() + end + + @spec get_site_admins() :: list(User.t) + defp get_site_admins() do + User + |> where([object], object.admin == true) + |> Repo.all() + end +end diff --git a/lib/code_corps/sparkpost/sparkpost.ex b/lib/code_corps/sparkpost/sparkpost.ex index f64bfc240..637835cce 100644 --- a/lib/code_corps/sparkpost/sparkpost.ex +++ b/lib/code_corps/sparkpost/sparkpost.ex @@ -7,6 +7,7 @@ defmodule CodeCorps.SparkPost do defdelegate send_forgot_password_email(user, token), to: Emails defdelegate send_message_initiated_by_project_email(message, conversation), to: Emails defdelegate send_organization_invite_email(invite), to: Emails + defdelegate send_project_approval_request_email(project), to: Emails defdelegate send_receipt_email(charge, invoice), to: Emails defdelegate send_reply_to_conversation_email(part, user), to: Emails end diff --git a/test/lib/code_corps/emails/project_approval_request_email_test.exs b/test/lib/code_corps/emails/project_approval_request_email_test.exs deleted file mode 100644 index f76a53992..000000000 --- a/test/lib/code_corps/emails/project_approval_request_email_test.exs +++ /dev/null @@ -1,29 +0,0 @@ -defmodule CodeCorps.Emails.ProjectApprovalRequestEmailTest do - use CodeCorps.ModelCase - use Bamboo.Test - - alias CodeCorps.Emails.ProjectApprovalRequestEmail - - test "request email works" do - project = insert(:project) - admin1 = insert(:user, admin: true) - admin2 = insert(:user, admin: true) - - email = ProjectApprovalRequestEmail.create(project) - assert email.from == "Code Corps" - assert Enum.count(email.to) == 2 - assert Enum.member?(email.to, admin1.email) - assert Enum.member?(email.to, admin2.email) - - template_model = email.private.template_model - - assert template_model == %{ - admin_project_show_url: "http://localhost:4200/admin/projects/#{project.id}", - project_description: project.description, - project_logo_url: "#{Application.get_env(:code_corps, :asset_host)}/icons/project_default_large_.png", - project_title: project.title, - project_url: "http://localhost:4200/#{project.organization.slug}/#{project.slug}", - subject: "#{project.title} is asking to be approved" - } - end -end diff --git a/test/lib/code_corps/sparkpost/emails/project_approval_request_test.exs b/test/lib/code_corps/sparkpost/emails/project_approval_request_test.exs new file mode 100644 index 000000000..59a095613 --- /dev/null +++ b/test/lib/code_corps/sparkpost/emails/project_approval_request_test.exs @@ -0,0 +1,43 @@ +defmodule CodeCorps.SparkPost.Emails.ProjectApprovalRequestTest do + use CodeCorps.DbAccessCase + + alias CodeCorps.SparkPost.Emails.ProjectApprovalRequest + + describe "build/1" do + test "provides substitution data for all keys used by template" do + project = insert(:project) + insert(:user, admin: true) + + %{substitution_data: data} = ProjectApprovalRequest.build(project) + + expected_keys = + "project-approval-request" + |> CodeCorps.SparkPostHelpers.get_keys_used_by_template + assert data |> Map.keys == expected_keys + end + + test "builds correct transmission model" do + project = insert(:project) + admin_1 = insert(:user, admin: true) + admin_2 = insert(:user, admin: true) + + %{substitution_data: data, recipients: [recipient_1, recipient_2]} = + ProjectApprovalRequest.build(project) + + assert data.from_name == "Code Corps" + assert data.from_email == "team@codecorps.org" + + assert data.admin_project_show_url == "http://localhost:4200/admin/projects/#{project.id}" + assert data.project_description == project.description + assert data.project_logo_url == "#{Application.get_env(:code_corps, :asset_host)}/icons/project_default_large_.png" + assert data.project_title == project.title + assert data.project_url == "http://localhost:4200/#{project.organization.slug}/#{project.slug}" + assert data.subject == "#{project.title} is asking to be approved" + + assert recipient_1.address.email == admin_1.email + assert recipient_1.address.name == admin_1.first_name + assert recipient_2.address.email == admin_2.email + assert recipient_2.address.name == admin_2.first_name + end + end +end diff --git a/test/lib/code_corps_web/controllers/project_controller_test.exs b/test/lib/code_corps_web/controllers/project_controller_test.exs index 1affba583..2cfd00303 100644 --- a/test/lib/code_corps_web/controllers/project_controller_test.exs +++ b/test/lib/code_corps_web/controllers/project_controller_test.exs @@ -148,9 +148,9 @@ defmodule CodeCorpsWeb.ProjectControllerTest do email = project - |> Emails.ProjectApprovalRequestEmail.create() + |> CodeCorps.SparkPost.Emails.ProjectApprovalRequest.build() - assert_delivered_email(email) + assert_received ^email user_id = current_user.id traits = project |> SegmentTraitsBuilder.build From bcfca0d08a02c6ba1ab049a64d2fbe557a1952a8 Mon Sep 17 00:00:00 2001 From: Nikola Begedin Date: Fri, 22 Dec 2017 13:33:15 +0100 Subject: [PATCH 10/20] Switch project approved email to SparkPost --- .../emails/project_approved_email.ex | 55 ------------------- lib/code_corps/projects/projects.ex | 5 +- lib/code_corps/sparkpost/emails/emails.ex | 4 ++ .../sparkpost/emails/project_approved.ex | 44 +++++++++++++++ lib/code_corps/sparkpost/sparkpost.ex | 1 + .../emails/project_approved_email_test.exs | 26 --------- .../emails/project_approved_test.exs | 39 +++++++++++++ .../controllers/project_controller_test.exs | 11 ++-- 8 files changed, 96 insertions(+), 89 deletions(-) delete mode 100644 lib/code_corps/emails/project_approved_email.ex create mode 100644 lib/code_corps/sparkpost/emails/project_approved.ex delete mode 100644 test/lib/code_corps/emails/project_approved_email_test.exs create mode 100644 test/lib/code_corps/sparkpost/emails/project_approved_test.exs diff --git a/lib/code_corps/emails/project_approved_email.ex b/lib/code_corps/emails/project_approved_email.ex deleted file mode 100644 index ed4c5d40a..000000000 --- a/lib/code_corps/emails/project_approved_email.ex +++ /dev/null @@ -1,55 +0,0 @@ -defmodule CodeCorps.Emails.ProjectApprovedEmail do - import Bamboo.Email, only: [to: 2] - import Bamboo.PostmarkHelper - import Ecto.Query - - alias CodeCorps.{Project, ProjectUser, Repo, User, WebClient} - alias CodeCorps.Emails.BaseEmail - - @spec create(Project.t) :: Bamboo.Email.t - def create(%Project{} = project) do - BaseEmail.create - |> to(project |> get_owners_emails()) - |> template(template_id(), build_model(project)) - end - - @spec build_model(Project.t) :: map - defp build_model(%Project{} = project) do - %{ - project_title: project.title, - project_url: project |> preload() |> project_url(), - subject: "#{project.title} is approved!" - } - end - - @spec preload(Project.t) :: Project.t - defp preload(%Project{} = project), do: project |> Repo.preload(:organization) - - @spec project_url(Project.t) :: String.t - defp project_url(project) do - WebClient.url() - |> URI.merge(project.organization.slug <> "/" <> project.slug) - |> URI.to_string() - end - - @spec template_id :: String.t - defp template_id, do: Application.get_env(:code_corps, :postmark_project_approved_template) - - @spec get_owners_emails(Project.t) :: list(String.t) - defp get_owners_emails(%Project{} = project) do - project |> get_owners() |> Enum.map(&extract_email/1) - end - - @spec extract_email(User.t) :: String.t - defp extract_email(%User{email: email}), do: email - - @spec get_owners(Project.t) :: list(User.t) - defp get_owners(%Project{id: project_id}) do - query = from u in User, - join: pu in ProjectUser, on: u.id == pu.user_id, - where: pu.project_id == ^project_id, - where: pu.role == "owner" - - query |> Repo.all() - end -end diff --git a/lib/code_corps/projects/projects.ex b/lib/code_corps/projects/projects.ex index fb98dfbb5..666d39bd9 100644 --- a/lib/code_corps/projects/projects.ex +++ b/lib/code_corps/projects/projects.ex @@ -6,7 +6,7 @@ defmodule CodeCorps.Projects do import CodeCorpsWeb.ProjectController, only: [preload: 1] alias CodeCorps.{ - Analytics.SegmentTracker, Emails, Mailer, Project, Repo, SparkPost, User + Analytics.SegmentTracker, Project, Repo, SparkPost, User } alias Ecto.Changeset @@ -100,7 +100,6 @@ defmodule CodeCorps.Projects do defp send_approved_email(project) do project |> preload() - |> Emails.ProjectApprovedEmail.create() - |> Mailer.deliver_now() + |> SparkPost.send_project_approved_email() end end diff --git a/lib/code_corps/sparkpost/emails/emails.ex b/lib/code_corps/sparkpost/emails/emails.ex index 8ef988945..98d847859 100644 --- a/lib/code_corps/sparkpost/emails/emails.ex +++ b/lib/code_corps/sparkpost/emails/emails.ex @@ -19,6 +19,10 @@ defmodule CodeCorps.SparkPost.Emails do project |> Emails.ProjectApprovalRequest.build |> API.send_transmission end + def send_project_approved_email(project) do + project |> Emails.ProjectApproved.build |> API.send_transmission + end + def send_receipt_email(charge, invoice) do case charge |> Emails.Receipt.build(invoice) do %SparkPost.Transmission{} = transmission -> diff --git a/lib/code_corps/sparkpost/emails/project_approved.ex b/lib/code_corps/sparkpost/emails/project_approved.ex new file mode 100644 index 000000000..9a76d2a85 --- /dev/null +++ b/lib/code_corps/sparkpost/emails/project_approved.ex @@ -0,0 +1,44 @@ +defmodule CodeCorps.SparkPost.Emails.ProjectApproved do + import Ecto.Query + + alias SparkPost.{Content, Transmission} + alias CodeCorps.{ + Project, ProjectUser, Repo, SparkPost.Emails.Recipient, User, WebClient + } + + @spec build(Project.t) :: %Transmission{} + def build(%Project{} = project) do + %Transmission{ + content: %Content.TemplateRef{template_id: "project-approval-request"}, + options: %Transmission.Options{inline_css: true}, + recipients: project |> get_owners() |> Enum.map(&Recipient.build/1), + substitution_data: %{ + from_name: "Code Corps", + from_email: "team@codecorps.org", + project_title: project.title, + project_url: project |> preload() |> project_url(), + subject: "#{project.title} is approved!" + } + } + end + + @spec preload(Project.t) :: Project.t + defp preload(%Project{} = project), do: project |> Repo.preload(:organization) + + @spec project_url(Project.t) :: String.t + defp project_url(project) do + WebClient.url() + |> URI.merge(project.organization.slug <> "/" <> project.slug) + |> URI.to_string() + end + + @spec get_owners(Project.t) :: list(User.t) + defp get_owners(%Project{id: project_id}) do + query = from u in User, + join: pu in ProjectUser, on: u.id == pu.user_id, + where: pu.project_id == ^project_id, + where: pu.role == "owner" + + query |> Repo.all() + end +end diff --git a/lib/code_corps/sparkpost/sparkpost.ex b/lib/code_corps/sparkpost/sparkpost.ex index 637835cce..13594b20c 100644 --- a/lib/code_corps/sparkpost/sparkpost.ex +++ b/lib/code_corps/sparkpost/sparkpost.ex @@ -8,6 +8,7 @@ defmodule CodeCorps.SparkPost do defdelegate send_message_initiated_by_project_email(message, conversation), to: Emails defdelegate send_organization_invite_email(invite), to: Emails defdelegate send_project_approval_request_email(project), to: Emails + defdelegate send_project_approved_email(project), to: Emails defdelegate send_receipt_email(charge, invoice), to: Emails defdelegate send_reply_to_conversation_email(part, user), to: Emails end diff --git a/test/lib/code_corps/emails/project_approved_email_test.exs b/test/lib/code_corps/emails/project_approved_email_test.exs deleted file mode 100644 index d67561bfc..000000000 --- a/test/lib/code_corps/emails/project_approved_email_test.exs +++ /dev/null @@ -1,26 +0,0 @@ -defmodule CodeCorps.Emails.ProjectApprovedEmailTest do - use CodeCorps.ModelCase - use Bamboo.Test - - alias CodeCorps.Emails.ProjectApprovedEmail - - test "email has the correct data" do - project = insert(:project) - %{user: owner1} = insert(:project_user, project: project, role: "owner") - %{user: owner2} = insert(:project_user, project: project, role: "owner") - - email = ProjectApprovedEmail.create(project) - assert email.from == "Code Corps" - assert Enum.count(email.to) == 2 - assert Enum.member?(email.to, owner1.email) - assert Enum.member?(email.to, owner2.email) - - template_model = email.private.template_model - - assert template_model == %{ - project_title: project.title, - project_url: "http://localhost:4200/#{project.organization.slug}/#{project.slug}", - subject: "#{project.title} is approved!" - } - end -end diff --git a/test/lib/code_corps/sparkpost/emails/project_approved_test.exs b/test/lib/code_corps/sparkpost/emails/project_approved_test.exs new file mode 100644 index 000000000..e75db9086 --- /dev/null +++ b/test/lib/code_corps/sparkpost/emails/project_approved_test.exs @@ -0,0 +1,39 @@ +defmodule CodeCorps.SparkPost.Emails.ProjectApprovedTest do + use CodeCorps.DbAccessCase + + alias CodeCorps.SparkPost.Emails.ProjectApproved + + describe "build/1" do + test "provides substitution data for all keys used by template" do + project = insert(:project) + + %{substitution_data: data} = ProjectApproved.build(project) + + expected_keys = + "project-approved" + |> CodeCorps.SparkPostHelpers.get_keys_used_by_template + assert data |> Map.keys == expected_keys + end + + test "builds correct transmission model" do + project = insert(:project) + %{user: owner_1} = insert(:project_user, project: project, role: "owner") + %{user: owner_2} = insert(:project_user, project: project, role: "owner") + + %{substitution_data: data, recipients: [recipient_1, recipient_2]} = + ProjectApproved.build(project) + + assert data.from_name == "Code Corps" + assert data.from_email == "team@codecorps.org" + + assert data.project_title == project.title + assert data.project_url == "http://localhost:4200/#{project.organization.slug}/#{project.slug}" + assert data.subject == "#{project.title} is approved!" + + assert recipient_1.address.email == owner_1.email + assert recipient_1.address.name == owner_1.first_name + assert recipient_2.address.email == owner_2.email + assert recipient_2.address.name == owner_2.first_name + end + end +end diff --git a/test/lib/code_corps_web/controllers/project_controller_test.exs b/test/lib/code_corps_web/controllers/project_controller_test.exs index 2cfd00303..3a4ddc4c3 100644 --- a/test/lib/code_corps_web/controllers/project_controller_test.exs +++ b/test/lib/code_corps_web/controllers/project_controller_test.exs @@ -2,9 +2,10 @@ defmodule CodeCorpsWeb.ProjectControllerTest do @moduledoc false use CodeCorpsWeb.ApiCase, resource_name: :project - use Bamboo.Test - alias CodeCorps.{Analytics.SegmentTraitsBuilder, Emails, Project, Repo} + alias CodeCorps.{ + Analytics.SegmentTraitsBuilder, SparkPost.Emails, Project, Repo + } @valid_attrs %{ cloudinary_public_id: "foo123", @@ -148,7 +149,7 @@ defmodule CodeCorpsWeb.ProjectControllerTest do email = project - |> CodeCorps.SparkPost.Emails.ProjectApprovalRequest.build() + |> Emails.ProjectApprovalRequest.build() assert_received ^email @@ -172,9 +173,9 @@ defmodule CodeCorpsWeb.ProjectControllerTest do email = project - |> Emails.ProjectApprovedEmail.create() + |> Emails.ProjectApproved.build() - assert_delivered_email(email) + assert_received ^email user_id = current_user.id traits = project |> SegmentTraitsBuilder.build From a590858f5a5a990a788cd96ee5f0e37465669458 Mon Sep 17 00:00:00 2001 From: Nikola Begedin Date: Fri, 22 Dec 2017 13:53:20 +0100 Subject: [PATCH 11/20] Switch project user acceptance email to SparkPost --- .../emails/project_user_acceptance_email.ex | 40 ------------------ lib/code_corps/sparkpost/emails/emails.ex | 4 ++ .../emails/project_user_acceptance.ex | 42 +++++++++++++++++++ lib/code_corps/sparkpost/sparkpost.ex | 1 + .../controllers/project_user_controller.ex | 5 +-- .../project_user_acceptance_email_test.exs | 25 ----------- .../emails/project_user_acceptance_test.exs | 38 +++++++++++++++++ .../project_user_controller_test.exs | 4 +- 8 files changed, 89 insertions(+), 70 deletions(-) delete mode 100644 lib/code_corps/emails/project_user_acceptance_email.ex create mode 100644 lib/code_corps/sparkpost/emails/project_user_acceptance.ex delete mode 100644 test/lib/code_corps/emails/project_user_acceptance_email_test.exs create mode 100644 test/lib/code_corps/sparkpost/emails/project_user_acceptance_test.exs diff --git a/lib/code_corps/emails/project_user_acceptance_email.ex b/lib/code_corps/emails/project_user_acceptance_email.ex deleted file mode 100644 index 4152da307..000000000 --- a/lib/code_corps/emails/project_user_acceptance_email.ex +++ /dev/null @@ -1,40 +0,0 @@ -defmodule CodeCorps.Emails.ProjectUserAcceptanceEmail do - import Bamboo.Email, only: [to: 2] - import Bamboo.PostmarkHelper - - alias CodeCorps.{Project, ProjectUser, Repo, User, WebClient} - alias CodeCorps.Emails.BaseEmail - alias CodeCorps.Presenters.ImagePresenter - - @spec create(ProjectUser.t) :: Bamboo.Email.t - def create(%ProjectUser{project: project, user: user}) do - BaseEmail.create - |> to(user.email) - |> template(template_id(), build_model(project, user)) - end - - @spec build_model(Project.t, User.t) :: map - defp build_model(%Project{} = project, %User{} = user) do - %{ - project_logo_url: ImagePresenter.large(project), - project_title: project.title, - project_url: project |> preload() |> url(), - subject: "#{project.title} just added you as a contributor", - user_first_name: user.first_name, - user_image_url: ImagePresenter.large(user) - } - end - - @spec preload(Project.t) :: Project.t - defp preload(%Project{} = project), do: project |> Repo.preload(:organization) - - @spec url(Project.t) :: String.t - defp url(project) do - WebClient.url() - |> URI.merge(project.organization.slug <> "/" <> project.slug) - |> URI.to_string - end - - @spec template_id :: String.t - defp template_id, do: Application.get_env(:code_corps, :postmark_project_user_acceptance_template) -end diff --git a/lib/code_corps/sparkpost/emails/emails.ex b/lib/code_corps/sparkpost/emails/emails.ex index 98d847859..8baf394fe 100644 --- a/lib/code_corps/sparkpost/emails/emails.ex +++ b/lib/code_corps/sparkpost/emails/emails.ex @@ -23,6 +23,10 @@ defmodule CodeCorps.SparkPost.Emails do project |> Emails.ProjectApproved.build |> API.send_transmission end + def send_project_user_acceptance_email(project_user) do + project_user |> Emails.ProjectUserAcceptance.build |> API.send_transmission + end + def send_receipt_email(charge, invoice) do case charge |> Emails.Receipt.build(invoice) do %SparkPost.Transmission{} = transmission -> diff --git a/lib/code_corps/sparkpost/emails/project_user_acceptance.ex b/lib/code_corps/sparkpost/emails/project_user_acceptance.ex new file mode 100644 index 000000000..50357e7cc --- /dev/null +++ b/lib/code_corps/sparkpost/emails/project_user_acceptance.ex @@ -0,0 +1,42 @@ +defmodule CodeCorps.SparkPost.Emails.ProjectUserAcceptance do + alias SparkPost.{Content, Transmission} + + alias CodeCorps.{ + Presenters.ImagePresenter, + Project, + ProjectUser, + Repo, + SparkPost.Emails.Recipient, + User, + WebClient + } + + @spec build(ProjectUser.t) :: %Transmission{} + def build(%ProjectUser{project: %Project{} = project, user: %User{} = user}) do + %Transmission{ + content: %Content.TemplateRef{template_id: "project-user-acceptance"}, + options: %Transmission.Options{inline_css: true}, + recipients: [user |> Recipient.build], + substitution_data: %{ + from_name: "Code Corps", + from_email: "team@codecorps.org", + project_logo_url: ImagePresenter.large(project), + project_title: project.title, + project_url: project |> preload() |> url(), + subject: "#{project.title} just added you as a contributor", + user_first_name: user.first_name, + user_image_url: ImagePresenter.large(user) + } + } + end + + @spec preload(Project.t) :: Project.t + defp preload(%Project{} = project), do: project |> Repo.preload(:organization) + + @spec url(Project.t) :: String.t + defp url(project) do + WebClient.url() + |> URI.merge(project.organization.slug <> "/" <> project.slug) + |> URI.to_string + end +end diff --git a/lib/code_corps/sparkpost/sparkpost.ex b/lib/code_corps/sparkpost/sparkpost.ex index 13594b20c..dec38dd8d 100644 --- a/lib/code_corps/sparkpost/sparkpost.ex +++ b/lib/code_corps/sparkpost/sparkpost.ex @@ -9,6 +9,7 @@ defmodule CodeCorps.SparkPost do defdelegate send_organization_invite_email(invite), to: Emails defdelegate send_project_approval_request_email(project), to: Emails defdelegate send_project_approved_email(project), to: Emails + defdelegate send_project_user_acceptance_email(project_user), to: Emails defdelegate send_receipt_email(charge, invoice), to: Emails defdelegate send_reply_to_conversation_email(part, user), to: Emails end diff --git a/lib/code_corps_web/controllers/project_user_controller.ex b/lib/code_corps_web/controllers/project_user_controller.ex index f842f5bfb..dd59fe011 100644 --- a/lib/code_corps_web/controllers/project_user_controller.ex +++ b/lib/code_corps_web/controllers/project_user_controller.ex @@ -2,7 +2,7 @@ defmodule CodeCorpsWeb.ProjectUserController do @moduledoc false use CodeCorpsWeb, :controller - alias CodeCorps.{Emails, Helpers.Query, Mailer, ProjectUser, User} + alias CodeCorps.{Emails, Helpers.Query, Mailer, ProjectUser, SparkPost, User} action_fallback CodeCorpsWeb.FallbackController plug CodeCorpsWeb.Plug.DataToAttributes @@ -85,7 +85,6 @@ defmodule CodeCorpsWeb.ProjectUserController do defp send_acceptance_email(project_user) do project_user |> Repo.preload(@preloads) - |> Emails.ProjectUserAcceptanceEmail.create() - |> Mailer.deliver_now() + |> SparkPost.send_project_user_acceptance_email() end end diff --git a/test/lib/code_corps/emails/project_user_acceptance_email_test.exs b/test/lib/code_corps/emails/project_user_acceptance_email_test.exs deleted file mode 100644 index 6473a9dd3..000000000 --- a/test/lib/code_corps/emails/project_user_acceptance_email_test.exs +++ /dev/null @@ -1,25 +0,0 @@ -defmodule CodeCorps.Emails.ProjectUserAcceptanceEmailTest do - use CodeCorps.ModelCase - use Bamboo.Test - - alias CodeCorps.Emails.ProjectUserAcceptanceEmail - - test "acceptance email works" do - %{project: project, user: user} = project_user = insert(:project_user) - - email = ProjectUserAcceptanceEmail.create(project_user) - assert email.from == "Code Corps" - assert email.to == user.email - - template_model = email.private.template_model - - assert template_model == %{ - project_title: project.title, - project_url: "http://localhost:4200/#{project.organization.slug}/#{project.slug}", - project_logo_url: "#{Application.get_env(:code_corps, :asset_host)}/icons/project_default_large_.png", - user_image_url: "#{Application.get_env(:code_corps, :asset_host)}/icons/user_default_large_.png", - user_first_name: user.first_name, - subject: "#{project.title} just added you as a contributor" - } - end -end diff --git a/test/lib/code_corps/sparkpost/emails/project_user_acceptance_test.exs b/test/lib/code_corps/sparkpost/emails/project_user_acceptance_test.exs new file mode 100644 index 000000000..d81039da1 --- /dev/null +++ b/test/lib/code_corps/sparkpost/emails/project_user_acceptance_test.exs @@ -0,0 +1,38 @@ +defmodule CodeCorps.SparkPost.Emails.ProjectUserAcceptanceTest do + use CodeCorps.DbAccessCase + + alias CodeCorps.SparkPost.Emails.ProjectUserAcceptance + + describe "build/1" do + test "provides substitution data for all keys used by template" do + project_user = insert(:project_user) + + %{substitution_data: data} = ProjectUserAcceptance.build(project_user) + + expected_keys = + "project-user-acceptance" + |> CodeCorps.SparkPostHelpers.get_keys_used_by_template + assert data |> Map.keys == expected_keys + end + + test "builds correct transmission model" do + %{project: project, user: user} = project_user = insert(:project_user) + + %{substitution_data: data, recipients: [recipient]} = + ProjectUserAcceptance.build(project_user) + + assert data.from_name == "Code Corps" + assert data.from_email == "team@codecorps.org" + + assert data.project_title == project.title + assert data.project_url == "http://localhost:4200/#{project.organization.slug}/#{project.slug}" + assert data.project_logo_url == "#{Application.get_env(:code_corps, :asset_host)}/icons/project_default_large_.png" + assert data.user_image_url == "#{Application.get_env(:code_corps, :asset_host)}/icons/user_default_large_.png" + assert data.user_first_name == user.first_name + assert data.subject == "#{project.title} just added you as a contributor" + + assert recipient.address.email == user.email + assert recipient.address.name == user.first_name + end + end +end diff --git a/test/lib/code_corps_web/controllers/project_user_controller_test.exs b/test/lib/code_corps_web/controllers/project_user_controller_test.exs index b06d9ddc5..a7f2ed379 100644 --- a/test/lib/code_corps_web/controllers/project_user_controller_test.exs +++ b/test/lib/code_corps_web/controllers/project_user_controller_test.exs @@ -119,9 +119,9 @@ defmodule CodeCorpsWeb.ProjectUserControllerTest do CodeCorps.ProjectUser |> CodeCorps.Repo.get_by(role: "contributor") |> CodeCorps.Repo.preload([:project, :user]) - |> CodeCorps.Emails.ProjectUserAcceptanceEmail.create() + |> CodeCorps.SparkPost.Emails.ProjectUserAcceptance.build() - assert_delivered_email(email) + assert_received ^email end test "doesn't update and renders 401 when unauthenticated", %{conn: conn} do From 39d354134ca1046658d7ae028e4f338b50187fbd Mon Sep 17 00:00:00 2001 From: Nikola Begedin Date: Fri, 22 Dec 2017 14:09:12 +0100 Subject: [PATCH 12/20] Switch project user request email to SparkPost --- .../emails/project_user_request_email.ex | 59 ------------------- lib/code_corps/sparkpost/emails/emails.ex | 4 ++ .../sparkpost/emails/project_user_request.ex | 53 +++++++++++++++++ lib/code_corps/sparkpost/sparkpost.ex | 1 + .../controllers/project_user_controller.ex | 5 +- .../project_user_request_email_test.exs | 30 ---------- .../emails/project_user_request_test.exs | 44 ++++++++++++++ .../project_user_controller_test.exs | 21 +++---- 8 files changed, 115 insertions(+), 102 deletions(-) delete mode 100644 lib/code_corps/emails/project_user_request_email.ex create mode 100644 lib/code_corps/sparkpost/emails/project_user_request.ex delete mode 100644 test/lib/code_corps/emails/project_user_request_email_test.exs create mode 100644 test/lib/code_corps/sparkpost/emails/project_user_request_test.exs diff --git a/lib/code_corps/emails/project_user_request_email.ex b/lib/code_corps/emails/project_user_request_email.ex deleted file mode 100644 index c2a377c41..000000000 --- a/lib/code_corps/emails/project_user_request_email.ex +++ /dev/null @@ -1,59 +0,0 @@ -defmodule CodeCorps.Emails.ProjectUserRequestEmail do - import Bamboo.Email, only: [to: 2] - import Bamboo.PostmarkHelper - import Ecto.Query - - alias CodeCorps.{Project, ProjectUser, Repo, User, WebClient} - alias CodeCorps.Emails.BaseEmail - alias CodeCorps.Presenters.ImagePresenter - - @spec create(ProjectUser.t) :: Bamboo.Email.t - def create(%ProjectUser{project: project, user: user}) do - BaseEmail.create - |> to(project |> get_owners_emails()) - |> template(template_id(), build_model(project, user)) - end - - @spec build_model(Project.t, User.t) :: map - defp build_model(%Project{} = project, %User{} = user) do - %{ - contributors_url: project |> preload() |> url(), - project_logo_url: ImagePresenter.large(project), - project_title: project.title, - subject: "#{user.first_name} wants to join #{project.title}", - user_first_name: user.first_name, - user_image_url: ImagePresenter.large(user) - } - end - - @spec preload(Project.t) :: Project.t - defp preload(%Project{} = project), do: project |> Repo.preload(:organization) - - @spec url(Project.t) :: String.t - defp url(project) do - WebClient.url() - |> URI.merge(project.organization.slug <> "/" <> project.slug <> "/people") - |> URI.to_string - end - - @spec template_id :: String.t - defp template_id, do: Application.get_env(:code_corps, :postmark_project_user_request_template) - - @spec get_owners_emails(Project.t) :: list(String.t) - defp get_owners_emails(%Project{} = project) do - project |> get_owners() |> Enum.map(&extract_email/1) - end - - @spec extract_email(User.t) :: String.t - defp extract_email(%User{email: email}), do: email - - @spec get_owners(Project.t) :: list(User.t) - defp get_owners(%Project{id: project_id}) do - query = from u in User, - join: pu in ProjectUser, on: u.id == pu.user_id, - where: pu.project_id == ^project_id, - where: pu.role == "owner" - - query |> Repo.all() - end -end diff --git a/lib/code_corps/sparkpost/emails/emails.ex b/lib/code_corps/sparkpost/emails/emails.ex index 8baf394fe..27f49d0ec 100644 --- a/lib/code_corps/sparkpost/emails/emails.ex +++ b/lib/code_corps/sparkpost/emails/emails.ex @@ -27,6 +27,10 @@ defmodule CodeCorps.SparkPost.Emails do project_user |> Emails.ProjectUserAcceptance.build |> API.send_transmission end + def send_project_user_request_email(project_user) do + project_user |> Emails.ProjectUserRequest.build |> API.send_transmission + end + def send_receipt_email(charge, invoice) do case charge |> Emails.Receipt.build(invoice) do %SparkPost.Transmission{} = transmission -> diff --git a/lib/code_corps/sparkpost/emails/project_user_request.ex b/lib/code_corps/sparkpost/emails/project_user_request.ex new file mode 100644 index 000000000..bd24c9aec --- /dev/null +++ b/lib/code_corps/sparkpost/emails/project_user_request.ex @@ -0,0 +1,53 @@ +defmodule CodeCorps.SparkPost.Emails.ProjectUserRequest do + import Ecto.Query + + alias SparkPost.{Content, Transmission} + alias CodeCorps.{ + Presenters.ImagePresenter, + Project, + ProjectUser, + Repo, + SparkPost.Emails.Recipient, + User, + WebClient + } + + @spec build(ProjectUser.t) :: %Transmission{} + def build(%ProjectUser{project: project, user: user}) do + %Transmission{ + content: %Content.TemplateRef{template_id: "project-approval-request"}, + options: %Transmission.Options{inline_css: true}, + recipients: project |> get_owners() |> Enum.map(&Recipient.build/1), + substitution_data: %{ + contributors_url: project |> preload() |> url(), + from_name: "Code Corps", + from_email: "team@codecorps.org", + project_logo_url: ImagePresenter.large(project), + project_title: project.title, + subject: "#{user.first_name} wants to join #{project.title}", + user_first_name: user.first_name, + user_image_url: ImagePresenter.large(user) + } + } + end + + @spec preload(Project.t) :: Project.t + defp preload(%Project{} = project), do: project |> Repo.preload(:organization) + + @spec url(Project.t) :: String.t + defp url(project) do + WebClient.url() + |> URI.merge(project.organization.slug <> "/" <> project.slug <> "/people") + |> URI.to_string + end + + @spec get_owners(Project.t) :: list(User.t) + defp get_owners(%Project{id: project_id}) do + query = from u in User, + join: pu in ProjectUser, on: u.id == pu.user_id, + where: pu.project_id == ^project_id, + where: pu.role == "owner" + + query |> Repo.all() + end +end diff --git a/lib/code_corps/sparkpost/sparkpost.ex b/lib/code_corps/sparkpost/sparkpost.ex index dec38dd8d..06c108fb9 100644 --- a/lib/code_corps/sparkpost/sparkpost.ex +++ b/lib/code_corps/sparkpost/sparkpost.ex @@ -10,6 +10,7 @@ defmodule CodeCorps.SparkPost do defdelegate send_project_approval_request_email(project), to: Emails defdelegate send_project_approved_email(project), to: Emails defdelegate send_project_user_acceptance_email(project_user), to: Emails + defdelegate send_project_user_request_email(project_user), to: Emails defdelegate send_receipt_email(charge, invoice), to: Emails defdelegate send_reply_to_conversation_email(part, user), to: Emails end diff --git a/lib/code_corps_web/controllers/project_user_controller.ex b/lib/code_corps_web/controllers/project_user_controller.ex index dd59fe011..b585fcf68 100644 --- a/lib/code_corps_web/controllers/project_user_controller.ex +++ b/lib/code_corps_web/controllers/project_user_controller.ex @@ -2,7 +2,7 @@ defmodule CodeCorpsWeb.ProjectUserController do @moduledoc false use CodeCorpsWeb, :controller - alias CodeCorps.{Emails, Helpers.Query, Mailer, ProjectUser, SparkPost, User} + alias CodeCorps.{Helpers.Query, ProjectUser, SparkPost, User} action_fallback CodeCorpsWeb.FallbackController plug CodeCorpsWeb.Plug.DataToAttributes @@ -68,8 +68,7 @@ defmodule CodeCorpsWeb.ProjectUserController do defp send_request_email(project_user) do project_user |> Repo.preload(@preloads) - |> Emails.ProjectUserRequestEmail.create() - |> Mailer.deliver_now() + |> SparkPost.send_project_user_request_email end @spec maybe_send_update_email(ProjectUser.t, ProjectUser.t) :: Bamboo.Email.t | nil diff --git a/test/lib/code_corps/emails/project_user_request_email_test.exs b/test/lib/code_corps/emails/project_user_request_email_test.exs deleted file mode 100644 index f1e6b1209..000000000 --- a/test/lib/code_corps/emails/project_user_request_email_test.exs +++ /dev/null @@ -1,30 +0,0 @@ -defmodule CodeCorps.Emails.ProjectUserRequestEmailTest do - use CodeCorps.ModelCase - use Bamboo.Test - - alias CodeCorps.Emails.ProjectUserRequestEmail - - test "request email works" do - project = insert(:project) - %{user: requesting_user} = project_user = insert(:project_user, project: project) - %{user: owner1} = insert(:project_user, project: project, role: "owner") - %{user: owner2} = insert(:project_user, project: project, role: "owner") - - email = ProjectUserRequestEmail.create(project_user) - assert email.from == "Code Corps" - assert Enum.count(email.to) == 2 - assert Enum.member?(email.to, owner1.email) - assert Enum.member?(email.to, owner2.email) - - template_model = email.private.template_model - - assert template_model == %{ - contributors_url: "http://localhost:4200/#{project.organization.slug}/#{project.slug}/people", - project_title: project.title, - project_logo_url: "#{Application.get_env(:code_corps, :asset_host)}/icons/project_default_large_.png", - user_image_url: "#{Application.get_env(:code_corps, :asset_host)}/icons/user_default_large_.png", - user_first_name: requesting_user.first_name, - subject: "#{requesting_user.first_name} wants to join #{project.title}" - } - end -end diff --git a/test/lib/code_corps/sparkpost/emails/project_user_request_test.exs b/test/lib/code_corps/sparkpost/emails/project_user_request_test.exs new file mode 100644 index 000000000..d745eeb6a --- /dev/null +++ b/test/lib/code_corps/sparkpost/emails/project_user_request_test.exs @@ -0,0 +1,44 @@ +defmodule CodeCorps.SparkPost.Emails.ProjectUserRequestTest do + use CodeCorps.DbAccessCase + + alias CodeCorps.SparkPost.Emails.ProjectUserRequest + + describe "build/1" do + test "provides substitution data for all keys used by template" do + project = insert(:project) + project_user = insert(:project_user, project: project) + + %{substitution_data: data} = ProjectUserRequest.build(project_user) + + expected_keys = + "project-user-request" + |> CodeCorps.SparkPostHelpers.get_keys_used_by_template + assert data |> Map.keys == expected_keys + end + + test "builds correct transmission model" do + project = insert(:project) + %{user: requesting_user} = project_user = insert(:project_user, project: project) + %{user: owner_1} = insert(:project_user, project: project, role: "owner") + %{user: owner_2} = insert(:project_user, project: project, role: "owner") + + %{substitution_data: data, recipients: [recipient_1, recipient_2]} = + ProjectUserRequest.build(project_user) + + assert data.from_name == "Code Corps" + assert data.from_email == "team@codecorps.org" + + assert data.contributors_url == "http://localhost:4200/#{project.organization.slug}/#{project.slug}/people" + assert data.project_title == project.title + assert data.project_logo_url == "#{Application.get_env(:code_corps, :asset_host)}/icons/project_default_large_.png" + assert data.user_image_url == "#{Application.get_env(:code_corps, :asset_host)}/icons/user_default_large_.png" + assert data.user_first_name == requesting_user.first_name + assert data.subject == "#{requesting_user.first_name} wants to join #{project.title}" + + assert recipient_1.address.email == owner_1.email + assert recipient_1.address.name == owner_1.first_name + assert recipient_2.address.email == owner_2.email + assert recipient_2.address.name == owner_2.first_name + end + end +end diff --git a/test/lib/code_corps_web/controllers/project_user_controller_test.exs b/test/lib/code_corps_web/controllers/project_user_controller_test.exs index a7f2ed379..23145d3ad 100644 --- a/test/lib/code_corps_web/controllers/project_user_controller_test.exs +++ b/test/lib/code_corps_web/controllers/project_user_controller_test.exs @@ -1,9 +1,10 @@ defmodule CodeCorpsWeb.ProjectUserControllerTest do use CodeCorpsWeb.ApiCase, resource_name: :project_user - use Bamboo.Test @attrs %{role: "contributor"} + alias CodeCorps.{ProjectUser, Repo, SparkPost.Emails} + describe "index" do test "lists all resources", %{conn: conn} do [record_1, record_2] = insert_pair(:project_user) @@ -62,12 +63,12 @@ defmodule CodeCorpsWeb.ProjectUserControllerTest do assert_received {:track, ^user_id, "Requested Project Membership", ^tracking_properties} email = - CodeCorps.ProjectUser - |> CodeCorps.Repo.get_by(role: "pending") - |> CodeCorps.Repo.preload([:project, :user]) - |> CodeCorps.Emails.ProjectUserRequestEmail.create() + ProjectUser + |> Repo.get_by(role: "pending") + |> Repo.preload([:project, :user]) + |> Emails.ProjectUserRequest.build() - assert_delivered_email(email) + assert_received ^email end @tag :authenticated @@ -116,10 +117,10 @@ defmodule CodeCorpsWeb.ProjectUserControllerTest do assert_received {:track, ^user_id, "Approved Project Membership", ^tracking_properties} email = - CodeCorps.ProjectUser - |> CodeCorps.Repo.get_by(role: "contributor") - |> CodeCorps.Repo.preload([:project, :user]) - |> CodeCorps.SparkPost.Emails.ProjectUserAcceptance.build() + ProjectUser + |> Repo.get_by(role: "contributor") + |> Repo.preload([:project, :user]) + |> Emails.ProjectUserAcceptance.build() assert_received ^email end From 2da2c56121f1310cefde9c6401bef2c791452a19 Mon Sep 17 00:00:00 2001 From: Nikola Begedin Date: Fri, 22 Dec 2017 14:25:00 +0100 Subject: [PATCH 13/20] Remove all remaining references to Bamboo and Postmark --- .env.example | 1 - config/dev.exs | 13 ------------- config/prod.exs | 15 --------------- config/remote-development.exs | 14 -------------- config/staging.exs | 15 --------------- config/test.exs | 14 -------------- emails/forgot_password.html | 3 ++- emails/message_initiated_by_project.html | 3 ++- emails/organization_invite.html | 3 ++- emails/project_approval_request.html | 3 ++- emails/project_approved.html | 3 ++- emails/project_user_acceptance.html | 3 ++- emails/project_user_request.html | 3 ++- emails/receipt.html | 3 ++- emails/reply_to_conversation.html | 3 ++- lib/code_corps/emails/base_email.ex | 14 -------------- lib/code_corps/mailer.ex | 3 --- lib/code_corps/projects/projects.ex | 2 +- .../controllers/project_user_controller.ex | 8 ++++---- lib/code_corps_web/router.ex | 4 ---- mix.exs | 18 ++++++++++-------- test/lib/code_corps/emails/base_email_test.exs | 17 ----------------- test/lib/code_corps/messages/messages_test.exs | 1 - ...github_app_installation_controller_test.exs | 1 - .../controllers/password_controller_test.exs | 1 - 25 files changed, 33 insertions(+), 135 deletions(-) delete mode 100644 lib/code_corps/emails/base_email.ex delete mode 100644 lib/code_corps/mailer.ex delete mode 100644 test/lib/code_corps/emails/base_email_test.exs diff --git a/.env.example b/.env.example index 704215565..1e270d324 100644 --- a/.env.example +++ b/.env.example @@ -13,7 +13,6 @@ export GITHUB_TEST_APP_CLIENT_SECRET= export GITHUB_TEST_APP_ID= export GITHUB_TEST_APP_PEM= export INTERCOM_IDENTITY_SECRET_KEY= -export POSTMARK_API_KEY= export S3_BUCKET= export SCOUT_APP_KEY= export SCOUT_APP_NAME= diff --git a/config/dev.exs b/config/dev.exs index f8f7b0b3f..3e3c7fcc4 100644 --- a/config/dev.exs +++ b/config/dev.exs @@ -56,19 +56,6 @@ config :code_corps, :stripe_env, :dev config :sentry, environment_name: Mix.env() || :dev -config :code_corps, CodeCorps.Mailer, adapter: Bamboo.LocalAdapter - -config :code_corps, - postmark_forgot_password_template: "123", - postmark_message_initiated_by_project_template: "123", - postmark_organization_invite_email_template: "123", - postmark_project_approval_request_template: "123", - postmark_project_approved_template: "123", - postmark_project_user_acceptance_template: "123", - postmark_project_user_request_template: "123", - postmark_receipt_template: "123", - postmark_reply_to_conversation_template: "123" - # If the dev environment has no CLOUDEX_API_KEY set, we want the app # to still run, with cloudex in test API mode if System.get_env("CLOUDEX_API_KEY") == nil do diff --git a/config/prod.exs b/config/prod.exs index 2665196cf..b0ce27202 100644 --- a/config/prod.exs +++ b/config/prod.exs @@ -55,21 +55,6 @@ config :code_corps, :stripe_env, :prod config :sentry, environment_name: Mix.env || :prod -config :code_corps, CodeCorps.Mailer, - adapter: Bamboo.PostmarkAdapter, - api_key: System.get_env("POSTMARK_API_KEY") - -config :code_corps, - postmark_forgot_password_template: "1989483", - postmark_message_initiated_by_project_template: "4324242", - postmark_organization_invite_email_template: "3441863", - postmark_project_approval_request_template: "4105824", - postmark_project_approved_template: "4105822", - postmark_project_user_acceptance_template: "1447041", - postmark_project_user_request_template: "4017262", - postmark_receipt_template: "1255222", - postmark_reply_to_conversation_template: "4324082" - # ## SSL Support # # To get SSL working, you will need to add the `https` key diff --git a/config/remote-development.exs b/config/remote-development.exs index 28ebf3cf6..378586eb8 100644 --- a/config/remote-development.exs +++ b/config/remote-development.exs @@ -39,20 +39,6 @@ config :logger, level: :info config :code_corps, :stripe, Stripe config :code_corps, :stripe_env, :remote_dev -config :code_corps, CodeCorps.Mailer, - adapter: Bamboo.LocalAdapter - -config :code_corps, - postmark_forgot_password_template: "123", - postmark_message_initiated_by_project_template: "123", - postmark_organization_invite_email_template: "123", - postmark_project_approval_request_template: "123", - postmark_project_approved_template: "123", - postmark_project_user_acceptance_template: "123", - postmark_project_user_request_template: "123", - postmark_receipt_template: "123", - postmark_reply_to_conversation_template: "123" - # ## SSL Support # # To get SSL working, you will need to add the `https` key diff --git a/config/staging.exs b/config/staging.exs index 947c43667..83e8568a9 100644 --- a/config/staging.exs +++ b/config/staging.exs @@ -54,21 +54,6 @@ config :sentry, config :code_corps, :stripe, Stripe config :code_corps, :stripe_env, :staging -config :code_corps, CodeCorps.Mailer, - adapter: Bamboo.PostmarkAdapter, - api_key: System.get_env("POSTMARK_API_KEY") - -config :code_corps, - postmark_forgot_password_template: "1989481", - postmark_message_initiated_by_project_template: "4324241", - postmark_organization_invite_email_template: "3442401", - postmark_project_approval_request_template: "4105823", - postmark_project_approved_template: "4105744", - postmark_project_user_acceptance_template: "1447022", - postmark_project_user_request_template: "4017261", - postmark_receipt_template: "1252361", - postmark_reply_to_conversation_template: "4324243" - # ## SSL Support # # To get SSL working, you will need to add the `https` key diff --git a/config/test.exs b/config/test.exs index 3dc4b803f..f3710c149 100644 --- a/config/test.exs +++ b/config/test.exs @@ -64,19 +64,5 @@ config :code_corps, :sentry, CodeCorps.Sentry.Sync config :code_corps, :processor, CodeCorps.Processor.Sync -config :code_corps, CodeCorps.Mailer, - adapter: Bamboo.TestAdapter - -config :code_corps, - postmark_forgot_password_template: "123", - postmark_message_initiated_by_project_template: "123", - postmark_organization_invite_email_template: "123", - postmark_project_approval_request_template: "123", - postmark_project_approved_template: "123", - postmark_project_user_acceptance_template: "123", - postmark_project_user_request_template: "123", - postmark_receipt_template: "123", - postmark_reply_to_conversation_template: "123" - config :code_corps, :cloudex, CloudexTest config :cloudex, api_key: "test_key", secret: "test_secret", cloud_name: "test_cloud_name" diff --git a/emails/forgot_password.html b/emails/forgot_password.html index acbdfa613..9024d66fd 100644 --- a/emails/forgot_password.html +++ b/emails/forgot_password.html @@ -5,7 +5,8 @@ Here's the link to reset your password.