From bc1d4ba229e7f9497d8970ac5d46549f8b3afa8b Mon Sep 17 00:00:00 2001 From: Daven Casia <77761194+fmterrorf@users.noreply.github.com> Date: Thu, 25 Mar 2021 20:28:52 -0600 Subject: [PATCH 01/14] Added logic --- lib/chat_api/conversations.ex | 13 ++++ .../controllers/twilio_controller.ex | 67 ++++++++++++++++++- 2 files changed, 79 insertions(+), 1 deletion(-) diff --git a/lib/chat_api/conversations.ex b/lib/chat_api/conversations.ex index 9bcab856a..8f65df9d9 100644 --- a/lib/chat_api/conversations.ex +++ b/lib/chat_api/conversations.ex @@ -189,6 +189,16 @@ defmodule ChatApi.Conversations do |> Repo.all() end + def find_latest_conversation(account_id, filters) do + Conversation + |> where(^filter_where(filters)) + |> where(account_id: ^account_id) + |> where([c], is_nil(c.archived_at)) + |> order_by(desc: :inserted_at) + |> first() + |> Repo.one() + end + # Used internally in dashboard @spec list_recent_by_customer(binary(), binary(), integer()) :: [Conversation.t()] def list_recent_by_customer(customer_id, account_id, limit \\ 5) do @@ -519,6 +529,9 @@ defmodule ChatApi.Conversations do {"account_id", value}, dynamic -> dynamic([p], ^dynamic and p.account_id == ^value) + {"source", value}, dynamic -> + dynamic([p], ^dynamic and p.source == ^value) + {_, _}, dynamic -> # Not a where parameter dynamic diff --git a/lib/chat_api_web/controllers/twilio_controller.ex b/lib/chat_api_web/controllers/twilio_controller.ex index 69371b17f..ac7782fff 100644 --- a/lib/chat_api_web/controllers/twilio_controller.ex +++ b/lib/chat_api_web/controllers/twilio_controller.ex @@ -1,6 +1,10 @@ defmodule ChatApiWeb.TwilioController do use ChatApiWeb, :controller + alias ChatApi.Messages + alias ChatApi.Messages.Message + alias ChatApi.Conversations + alias ChatApi.Customers alias ChatApi.Twilio alias ChatApi.Twilio.TwilioAuthorization @@ -62,6 +66,68 @@ defmodule ChatApiWeb.TwilioController do @spec webhook(Plug.Conn.t(), map()) :: Plug.Conn.t() def webhook(conn, payload) do Logger.debug("Payload from Twilio webhook: #{inspect(payload)}") + %{"AccountSid" => account_sid, "To" => to, "From" => from, "Body" => body} = payload + + case Twilio.find_twilio_authorization(%{ + twilio_account_sid: account_sid, + from_phone_number: to + }) do + nil -> + Logger.error("Cannot find twilio auth") + + twilio_auth -> + %{account_id: account_id} = twilio_auth + + result = + case Customers.list_customers(account_id, %{phone: from}) do + [] -> + case Customers.create_customer(%{phone: from, account_id: account_id}) do + {:ok, customer} -> + {customer, + Conversations.create_conversation(%{ + account_id: account_id, + customer_id: customer.id, + source: "sms" + })} + + error -> + error + end + + [customer] -> + case Conversations.find_latest_conversation(account_id, %{ + customer_id: customer.id, + source: "sms" + }) do + nil -> + {customer, + Conversations.create_conversation(%{ + account_id: account_id, + customer_id: customer.id + })} + + conversation -> + {customer, conversation} + end + end + + case result do + {:error, _} -> + Logger.error("Error") + send_resp(conn, 500, "") + + {customer, conversation} -> + Messages.create_message(%{ + body: body, + account_id: account_id, + customer_id: customer.id, + conversation_id: conversation.id + }) + + send_resp(conn, 200, "") + end + end + # TODO: implement me! # # When new SMS message comes in... @@ -73,7 +139,6 @@ defmodule ChatApiWeb.TwilioController do # - If customer exists, fetch latest open conversation (with `source: "sms"`) # - If open conversation exists, add message to conversation # - Otherwise, create new conversation (with `source: "sms"`) - send_resp(conn, 200, "") end @spec verify_authorization(map()) :: :ok | {:error, atom(), any()} From 6ea85bc5f5a1584d02314ff07adf76b1e1aebe16 Mon Sep 17 00:00:00 2001 From: Daven Casia <77761194+fmterrorf@users.noreply.github.com> Date: Fri, 26 Mar 2021 19:42:20 -0600 Subject: [PATCH 02/14] Added tests for context --- config/dev.exs | 4 +- lib/chat_api/conversations/conversation.ex | 2 +- lib/chat_api/twilio.ex | 53 +++++++++++++ .../controllers/twilio_controller.ex | 78 ++++--------------- test/chat_api/twilio_test.exs | 63 +++++++++++++++ .../controllers/twilio_controller_test.exs | 26 +++++++ 6 files changed, 162 insertions(+), 64 deletions(-) create mode 100644 test/chat_api/twilio_test.exs create mode 100644 test/chat_api_web/controllers/twilio_controller_test.exs diff --git a/config/dev.exs b/config/dev.exs index 296a3e7da..50e35374f 100644 --- a/config/dev.exs +++ b/config/dev.exs @@ -6,8 +6,8 @@ database_url = System.get_env("DATABASE_URL") || "ecto://postgres:postgres@local config :chat_api, ChatApi.Repo, url: database_url, show_sensitive_data_on_connection_error: true, - pool_size: 10, - socket_options: [:inet6] + pool_size: 10 + # socket_options: [:inet6] # For development, we disable any cache and enable # debugging and code reloading. diff --git a/lib/chat_api/conversations/conversation.ex b/lib/chat_api/conversations/conversation.ex index 3669726cf..81da3ce51 100644 --- a/lib/chat_api/conversations/conversation.ex +++ b/lib/chat_api/conversations/conversation.ex @@ -76,7 +76,7 @@ defmodule ChatApi.Conversations.Conversation do :metadata ]) |> validate_required([:status, :account_id, :customer_id]) - |> validate_inclusion(:source, ["chat", "slack", "email"]) + |> validate_inclusion(:source, ["chat", "slack", "email", "sms"]) |> put_closed_at() |> foreign_key_constraint(:account_id) |> foreign_key_constraint(:customer_id) diff --git a/lib/chat_api/twilio.ex b/lib/chat_api/twilio.ex index 3e2bf0131..87d5d64cb 100644 --- a/lib/chat_api/twilio.ex +++ b/lib/chat_api/twilio.ex @@ -7,6 +7,8 @@ defmodule ChatApi.Twilio do alias ChatApi.Repo alias ChatApi.Twilio.TwilioAuthorization + alias ChatApi.Conversations + alias ChatApi.Customers @spec list_twilio_authorizations() :: [TwilioAuthorization.t()] def list_twilio_authorizations() do @@ -82,6 +84,57 @@ defmodule ChatApi.Twilio do |> Repo.one() end + @spec find_or_create_customer_and_conversation(String.t(), String.t()) :: + {:ok, Customers.Customer.t(), Conversations.Conversation.t()} + | {:error, Ecto.Changeset.t()} + def find_or_create_customer_and_conversation(account_id, phone) do + now = DateTime.utc_now() + sms_source = "sms" + + case Customers.list_customers(account_id, %{phone: phone}) do + [] -> + with {:ok, customer} <- + Customers.create_customer(%{ + phone: phone, + account_id: account_id, + first_seen: now, + last_seen: now + }), + {:ok, conversation} <- + Conversations.create_conversation(%{ + account_id: account_id, + customer_id: customer.id, + source: sms_source + }) do + {:ok, customer, conversation} + else + error -> + error + end + + [customer] -> + with nil <- + Conversations.find_latest_conversation(account_id, %{ + customer_id: customer.id, + source: sms_source + }), + {:ok, conversation} <- + Conversations.create_conversation(%{ + account_id: account_id, + customer_id: customer.id, + source: sms_source + }) do + {:ok, customer, conversation} + else + %Conversations.Conversation{} = conversation -> + {:ok, customer, conversation} + + error -> + error + end + end + end + defp filter_authorizations_where(params) do Enum.reduce(params, dynamic(true), fn {:account_id, value}, dynamic -> diff --git a/lib/chat_api_web/controllers/twilio_controller.ex b/lib/chat_api_web/controllers/twilio_controller.ex index ac7782fff..8b8d3255b 100644 --- a/lib/chat_api_web/controllers/twilio_controller.ex +++ b/lib/chat_api_web/controllers/twilio_controller.ex @@ -2,9 +2,6 @@ defmodule ChatApiWeb.TwilioController do use ChatApiWeb, :controller alias ChatApi.Messages - alias ChatApi.Messages.Message - alias ChatApi.Conversations - alias ChatApi.Customers alias ChatApi.Twilio alias ChatApi.Twilio.TwilioAuthorization @@ -68,64 +65,23 @@ defmodule ChatApiWeb.TwilioController do Logger.debug("Payload from Twilio webhook: #{inspect(payload)}") %{"AccountSid" => account_sid, "To" => to, "From" => from, "Body" => body} = payload - case Twilio.find_twilio_authorization(%{ - twilio_account_sid: account_sid, - from_phone_number: to - }) do - nil -> - Logger.error("Cannot find twilio auth") - - twilio_auth -> - %{account_id: account_id} = twilio_auth - - result = - case Customers.list_customers(account_id, %{phone: from}) do - [] -> - case Customers.create_customer(%{phone: from, account_id: account_id}) do - {:ok, customer} -> - {customer, - Conversations.create_conversation(%{ - account_id: account_id, - customer_id: customer.id, - source: "sms" - })} - - error -> - error - end - - [customer] -> - case Conversations.find_latest_conversation(account_id, %{ - customer_id: customer.id, - source: "sms" - }) do - nil -> - {customer, - Conversations.create_conversation(%{ - account_id: account_id, - customer_id: customer.id - })} - - conversation -> - {customer, conversation} - end - end - - case result do - {:error, _} -> - Logger.error("Error") - send_resp(conn, 500, "") - - {customer, conversation} -> - Messages.create_message(%{ - body: body, - account_id: account_id, - customer_id: customer.id, - conversation_id: conversation.id - }) - - send_resp(conn, 200, "") - end + with %TwilioAuthorization{account_id: account_id} <- + Twilio.find_twilio_authorization(%{ + twilio_account_sid: account_sid, + from_phone_number: to + }), + {:ok, customer, conversation} <- Twilio.find_or_create_customer_and_conversation(account_id, from), + {:ok, _mesage} <- + Messages.create_message(%{ + body: body, + account_id: account_id, + customer_id: customer.id, + conversation_id: conversation.id + }) do + send_resp(conn, 200, "") + else + nil -> send_resp(conn, 404, "Twilio account not found") + _ -> send_resp(conn, 500, "") end # TODO: implement me! diff --git a/test/chat_api/twilio_test.exs b/test/chat_api/twilio_test.exs new file mode 100644 index 000000000..190d9e4a1 --- /dev/null +++ b/test/chat_api/twilio_test.exs @@ -0,0 +1,63 @@ +defmodule ChatApi.TwilioTest do + use ChatApi.DataCase, async: true + import ChatApi.Factory + alias ChatApi.Twilio + + test "find_or_create_customer_and_conversation/2 returns customer and new conversation when customer exists" do + account = insert(:account) + customer = insert(:customer, account: account) + + {:ok, found_customer, found_conversation} = + Twilio.find_or_create_customer_and_conversation(account.id, customer.phone) + + assert customer.id == found_customer.id + + assert customer.id == found_conversation.customer_id + assert "sms" == found_conversation.source + end + + test "find_or_create_customer_and_conversation/2 returns new customer and conversation when customer doesn't exist" do + account = insert(:account) + phone_number = "+18675309" + + {:ok, found_customer, found_conversation} = + Twilio.find_or_create_customer_and_conversation(account.id, phone_number) + + assert phone_number == found_customer.phone + assert account.id == found_customer.account_id + assert account.id == found_conversation.account_id + end + + test "find_or_create_customer_and_conversation/2 returns latest conversations when multiple conversations exist" do + account = insert(:account) + customer = insert(:customer, account: account) + + insert(:conversation, + account: account, + customer: customer, + inserted_at: ~N[2020-12-01 00:00:00], + source: "sms" + ) + + insert(:conversation, + account: account, + customer: customer, + inserted_at: ~N[2020-12-01 00:01:00], + source: "sms" + ) + + latest_conversation = + insert(:conversation, + account: account, + customer: customer, + inserted_at: ~N[2020-12-01 00:02:00], + source: "sms" + ) + + {:ok, found_customer, found_conversation} = + Twilio.find_or_create_customer_and_conversation(account.id, "+18675309") + + assert latest_conversation.id == found_conversation.id + assert customer.id == found_customer.id + end +end diff --git a/test/chat_api_web/controllers/twilio_controller_test.exs b/test/chat_api_web/controllers/twilio_controller_test.exs new file mode 100644 index 000000000..4e46c87e6 --- /dev/null +++ b/test/chat_api_web/controllers/twilio_controller_test.exs @@ -0,0 +1,26 @@ +defmodule ChatApiWeb.TwilioControllerTest do + use ChatApiWeb.ConnCase, async: true + + import ChatApi.Factory + + setup %{conn: conn} do + account = insert(:account) + user = insert(:user, account: account) + conn = put_req_header(conn, "accept", "application/json") + authed_conn = Pow.Plug.assign_current_user(conn, user, []) + + {:ok, conn: conn, authed_conn: authed_conn, account: account} + end + + describe "update" do + # test "returns existing widget_settings", + # %{authed_conn: authed_conn, account: account} do + + # resp = + # put(authed_conn, Routes.widget_settings_path(authed_conn, :update), %{ + # widget_settings: settings + # }) + + # end + end +end From f1d4028aefecb633a34f18671483497003edb699 Mon Sep 17 00:00:00 2001 From: Daven Casia <77761194+fmterrorf@users.noreply.github.com> Date: Fri, 26 Mar 2021 20:22:45 -0600 Subject: [PATCH 03/14] Added controller tests --- .../controllers/twilio_controller.ex | 16 +-- .../controllers/twilio_controller_test.exs | 99 +++++++++++++++++-- 2 files changed, 93 insertions(+), 22 deletions(-) diff --git a/lib/chat_api_web/controllers/twilio_controller.ex b/lib/chat_api_web/controllers/twilio_controller.ex index 8b8d3255b..42672352e 100644 --- a/lib/chat_api_web/controllers/twilio_controller.ex +++ b/lib/chat_api_web/controllers/twilio_controller.ex @@ -81,20 +81,10 @@ defmodule ChatApiWeb.TwilioController do send_resp(conn, 200, "") else nil -> send_resp(conn, 404, "Twilio account not found") - _ -> send_resp(conn, 500, "") + error -> + Logger.error(inspect(error)) + send_resp(conn, 500, "") end - - # TODO: implement me! - # - # When new SMS message comes in... - # - Check if the receiving number matches one of our `twilio_authorizations` - # - If it does, use that to determine the `account_id` (from the `twilio_authorizations` table) - # Next, find or create a conversation for the account (with `source: "sms"`) - # - First, find customer by phone number (implement `Customers.find_by_phone/2`) - # - If no customer exists, create new customer record and new conversation (with `source: "sms"`) - # - If customer exists, fetch latest open conversation (with `source: "sms"`) - # - If open conversation exists, add message to conversation - # - Otherwise, create new conversation (with `source: "sms"`) end @spec verify_authorization(map()) :: :ok | {:error, atom(), any()} diff --git a/test/chat_api_web/controllers/twilio_controller_test.exs b/test/chat_api_web/controllers/twilio_controller_test.exs index 4e46c87e6..74da240e0 100644 --- a/test/chat_api_web/controllers/twilio_controller_test.exs +++ b/test/chat_api_web/controllers/twilio_controller_test.exs @@ -1,8 +1,16 @@ defmodule ChatApiWeb.TwilioControllerTest do - use ChatApiWeb.ConnCase, async: true + use ChatApiWeb.ConnCase + import Mock import ChatApi.Factory + alias ChatApi.Twilio + alias ChatApi.Customers.Customer + alias ChatApi.Messages + alias ChatApi.Messages.Message + alias ChatApi.Conversations.Conversation + alias ChatApi.Twilio.TwilioAuthorization + setup %{conn: conn} do account = insert(:account) user = insert(:user, account: account) @@ -12,15 +20,88 @@ defmodule ChatApiWeb.TwilioControllerTest do {:ok, conn: conn, authed_conn: authed_conn, account: account} end - describe "update" do - # test "returns existing widget_settings", - # %{authed_conn: authed_conn, account: account} do + describe "webhook" do + @request_body %{ + "AccountSid" => "1234", + "To" => "1234", + "From" => "1234", + "Body" => "body" + } + + test "returns 200 when message is successfuly created", + %{authed_conn: authed_conn} do + with_mocks([ + { + Twilio, + [], + [ + find_twilio_authorization: fn _ -> + %TwilioAuthorization{ + account_id: 1 + } + end, + find_or_create_customer_and_conversation: fn _, __ -> + {:ok, + %Customer{ + id: 1 + }, + %Conversation{ + id: 1 + }} + end + ] + }, + { + Messages, + [], + create_message: fn _ -> {:ok, %Message{id: 1}} end + } + ]) do + conn = post(authed_conn, Routes.twilio_path(authed_conn, :webhook), @request_body) + + assert response(conn, 200) + end + end + + test "returns 404 when twilio account is not found", + %{authed_conn: authed_conn} do + with_mocks([ + { + Twilio, + [], + [ + find_twilio_authorization: fn _ -> nil end + ] + } + ]) do + conn = post(authed_conn, Routes.twilio_path(authed_conn, :webhook), @request_body) + + assert response(conn, 404) + end + end - # resp = - # put(authed_conn, Routes.widget_settings_path(authed_conn, :update), %{ - # widget_settings: settings - # }) + test "returns 500 when unexpected error occurs", + %{authed_conn: authed_conn} do + with_mocks([ + { + Twilio, + [], + [ + find_twilio_authorization: fn _ -> + %TwilioAuthorization{ + account_id: 1 + } + end, + find_or_create_customer_and_conversation: fn _, __ -> + {:error, %Ecto.Changeset{}} + end + ] + } + ]) do + conn = post(authed_conn, Routes.twilio_path(authed_conn, :webhook), @request_body) - # end + assert response(conn, 500) + end + end end end From aaf97d804d98979ccf0470b4cebfc2e266924b94 Mon Sep 17 00:00:00 2001 From: Daven Casia <77761194+fmterrorf@users.noreply.github.com> Date: Fri, 26 Mar 2021 20:23:51 -0600 Subject: [PATCH 04/14] Pattern match param --- lib/chat_api_web/controllers/twilio_controller.ex | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/lib/chat_api_web/controllers/twilio_controller.ex b/lib/chat_api_web/controllers/twilio_controller.ex index 42672352e..07c2fd791 100644 --- a/lib/chat_api_web/controllers/twilio_controller.ex +++ b/lib/chat_api_web/controllers/twilio_controller.ex @@ -61,16 +61,19 @@ defmodule ChatApiWeb.TwilioController do end @spec webhook(Plug.Conn.t(), map()) :: Plug.Conn.t() - def webhook(conn, payload) do + def webhook( + conn, + %{"AccountSid" => account_sid, "To" => to, "From" => from, "Body" => body} = payload + ) do Logger.debug("Payload from Twilio webhook: #{inspect(payload)}") - %{"AccountSid" => account_sid, "To" => to, "From" => from, "Body" => body} = payload with %TwilioAuthorization{account_id: account_id} <- Twilio.find_twilio_authorization(%{ twilio_account_sid: account_sid, from_phone_number: to }), - {:ok, customer, conversation} <- Twilio.find_or_create_customer_and_conversation(account_id, from), + {:ok, customer, conversation} <- + Twilio.find_or_create_customer_and_conversation(account_id, from), {:ok, _mesage} <- Messages.create_message(%{ body: body, @@ -80,7 +83,9 @@ defmodule ChatApiWeb.TwilioController do }) do send_resp(conn, 200, "") else - nil -> send_resp(conn, 404, "Twilio account not found") + nil -> + send_resp(conn, 404, "Twilio account not found") + error -> Logger.error(inspect(error)) send_resp(conn, 500, "") From 1e83eab989f395dad08d28a31b14925659233443 Mon Sep 17 00:00:00 2001 From: Daven Casia <77761194+fmterrorf@users.noreply.github.com> Date: Fri, 26 Mar 2021 20:25:54 -0600 Subject: [PATCH 05/14] Removed whitespace --- .../controllers/twilio_controller_test.exs | 16 +++------------- 1 file changed, 3 insertions(+), 13 deletions(-) diff --git a/test/chat_api_web/controllers/twilio_controller_test.exs b/test/chat_api_web/controllers/twilio_controller_test.exs index 74da240e0..cf8b34392 100644 --- a/test/chat_api_web/controllers/twilio_controller_test.exs +++ b/test/chat_api_web/controllers/twilio_controller_test.exs @@ -36,18 +36,10 @@ defmodule ChatApiWeb.TwilioControllerTest do [], [ find_twilio_authorization: fn _ -> - %TwilioAuthorization{ - account_id: 1 - } + %TwilioAuthorization{account_id: 1} end, find_or_create_customer_and_conversation: fn _, __ -> - {:ok, - %Customer{ - id: 1 - }, - %Conversation{ - id: 1 - }} + {:ok, %Customer{id: 1}, %Conversation{id: 1}} end ] }, @@ -88,9 +80,7 @@ defmodule ChatApiWeb.TwilioControllerTest do [], [ find_twilio_authorization: fn _ -> - %TwilioAuthorization{ - account_id: 1 - } + %TwilioAuthorization{account_id: 1} end, find_or_create_customer_and_conversation: fn _, __ -> {:error, %Ecto.Changeset{}} From d4ef283f007be4c10e4a95c284e67625b848ccd3 Mon Sep 17 00:00:00 2001 From: Daven Casia <77761194+fmterrorf@users.noreply.github.com> Date: Fri, 26 Mar 2021 20:26:57 -0600 Subject: [PATCH 06/14] Add inet6 back --- config/dev.exs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/config/dev.exs b/config/dev.exs index 50e35374f..296a3e7da 100644 --- a/config/dev.exs +++ b/config/dev.exs @@ -6,8 +6,8 @@ database_url = System.get_env("DATABASE_URL") || "ecto://postgres:postgres@local config :chat_api, ChatApi.Repo, url: database_url, show_sensitive_data_on_connection_error: true, - pool_size: 10 - # socket_options: [:inet6] + pool_size: 10, + socket_options: [:inet6] # For development, we disable any cache and enable # debugging and code reloading. From 9528e7ac191ded653246fe4bc6e12f0d6c92c0f5 Mon Sep 17 00:00:00 2001 From: Daven Casia <77761194+fmterrorf@users.noreply.github.com> Date: Fri, 26 Mar 2021 20:29:10 -0600 Subject: [PATCH 07/14] Add typespec --- lib/chat_api/conversations.ex | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/chat_api/conversations.ex b/lib/chat_api/conversations.ex index 8f65df9d9..976fecf79 100644 --- a/lib/chat_api/conversations.ex +++ b/lib/chat_api/conversations.ex @@ -189,6 +189,7 @@ defmodule ChatApi.Conversations do |> Repo.all() end + @spec find_latest_conversation(binary(), map()) :: Conversation.t() def find_latest_conversation(account_id, filters) do Conversation |> where(^filter_where(filters)) From e9be0cd36f8fc4378358c39fe7dba01b4652be53 Mon Sep 17 00:00:00 2001 From: Daven Casia <77761194+fmterrorf@users.noreply.github.com> Date: Fri, 26 Mar 2021 21:41:52 -0600 Subject: [PATCH 08/14] Wrap test with describe --- test/chat_api/twilio_test.exs | 86 ++++++++++++++++++----------------- 1 file changed, 44 insertions(+), 42 deletions(-) diff --git a/test/chat_api/twilio_test.exs b/test/chat_api/twilio_test.exs index 190d9e4a1..c4220f31c 100644 --- a/test/chat_api/twilio_test.exs +++ b/test/chat_api/twilio_test.exs @@ -3,61 +3,63 @@ defmodule ChatApi.TwilioTest do import ChatApi.Factory alias ChatApi.Twilio - test "find_or_create_customer_and_conversation/2 returns customer and new conversation when customer exists" do - account = insert(:account) - customer = insert(:customer, account: account) + describe "find_or_create_customer_and_conversation/2" do + test "returns customer and new conversation when customer exists" do + account = insert(:account) + customer = insert(:customer, account: account) - {:ok, found_customer, found_conversation} = - Twilio.find_or_create_customer_and_conversation(account.id, customer.phone) + {:ok, found_customer, found_conversation} = + Twilio.find_or_create_customer_and_conversation(account.id, customer.phone) - assert customer.id == found_customer.id + assert customer.id == found_customer.id - assert customer.id == found_conversation.customer_id - assert "sms" == found_conversation.source - end + assert customer.id == found_conversation.customer_id + assert "sms" == found_conversation.source + end - test "find_or_create_customer_and_conversation/2 returns new customer and conversation when customer doesn't exist" do - account = insert(:account) - phone_number = "+18675309" + test "returns new customer and conversation when customer doesn't exist" do + account = insert(:account) + phone_number = "+18675309" - {:ok, found_customer, found_conversation} = - Twilio.find_or_create_customer_and_conversation(account.id, phone_number) + {:ok, found_customer, found_conversation} = + Twilio.find_or_create_customer_and_conversation(account.id, phone_number) - assert phone_number == found_customer.phone - assert account.id == found_customer.account_id - assert account.id == found_conversation.account_id - end + assert phone_number == found_customer.phone + assert account.id == found_customer.account_id + assert account.id == found_conversation.account_id + end + + test "returns latest conversations when multiple conversations exist" do + account = insert(:account) + customer = insert(:customer, account: account) + + insert(:conversation, + account: account, + customer: customer, + inserted_at: ~N[2020-12-01 00:00:00], + source: "sms" + ) - test "find_or_create_customer_and_conversation/2 returns latest conversations when multiple conversations exist" do - account = insert(:account) - customer = insert(:customer, account: account) - - insert(:conversation, - account: account, - customer: customer, - inserted_at: ~N[2020-12-01 00:00:00], - source: "sms" - ) - - insert(:conversation, - account: account, - customer: customer, - inserted_at: ~N[2020-12-01 00:01:00], - source: "sms" - ) - - latest_conversation = insert(:conversation, account: account, customer: customer, - inserted_at: ~N[2020-12-01 00:02:00], + inserted_at: ~N[2020-12-01 00:01:00], source: "sms" ) - {:ok, found_customer, found_conversation} = - Twilio.find_or_create_customer_and_conversation(account.id, "+18675309") + latest_conversation = + insert(:conversation, + account: account, + customer: customer, + inserted_at: ~N[2020-12-01 00:02:00], + source: "sms" + ) + + {:ok, found_customer, found_conversation} = + Twilio.find_or_create_customer_and_conversation(account.id, "+18675309") - assert latest_conversation.id == found_conversation.id - assert customer.id == found_customer.id + assert latest_conversation.id == found_conversation.id + assert customer.id == found_customer.id + end end end From c654b80833b41260c3eb1bef2eeafcc35f119c1a Mon Sep 17 00:00:00 2001 From: Daven Casia <77761194+fmterrorf@users.noreply.github.com> Date: Mon, 29 Mar 2021 18:47:50 -0600 Subject: [PATCH 09/14] Moved find_or_create_customer_and_conversation to Conversations context --- lib/chat_api/conversations.ex | 56 +++++++++++++++- lib/chat_api/twilio.ex | 53 --------------- .../controllers/twilio_controller.ex | 3 +- test/chat_api/conversations_test.exs | 60 +++++++++++++++++ test/chat_api/twilio_test.exs | 65 ------------------- .../controllers/twilio_controller_test.exs | 17 ++++- 6 files changed, 132 insertions(+), 122 deletions(-) delete mode 100644 test/chat_api/twilio_test.exs diff --git a/lib/chat_api/conversations.ex b/lib/chat_api/conversations.ex index 976fecf79..2891c3bfc 100644 --- a/lib/chat_api/conversations.ex +++ b/lib/chat_api/conversations.ex @@ -8,6 +8,7 @@ defmodule ChatApi.Conversations do alias ChatApi.Accounts.Account alias ChatApi.Conversations.Conversation + alias ChatApi.Customers alias ChatApi.Customers.Customer alias ChatApi.Messages.Message alias ChatApi.Tags.{Tag, ConversationTag} @@ -189,7 +190,7 @@ defmodule ChatApi.Conversations do |> Repo.all() end - @spec find_latest_conversation(binary(), map()) :: Conversation.t() + @spec find_latest_conversation(binary(), map()) :: Conversation.t() | nil def find_latest_conversation(account_id, filters) do Conversation |> where(^filter_where(filters)) @@ -507,6 +508,59 @@ defmodule ChatApi.Conversations do |> Repo.update() end + @spec find_or_create_customer_and_conversation(String.t(), String.t()) :: + {:ok, Customers.Customer.t(), Conversation.t()} + | {:error, Ecto.Changeset.t()} + def find_or_create_customer_and_conversation(account_id, phone) do + now = DateTime.utc_now() + sms_source = "sms" + + case Customers.list_customers(account_id, %{phone: phone}) do + [] -> + with {:ok, customer} <- + Customers.create_customer(%{ + phone: phone, + account_id: account_id, + first_seen: now, + last_seen: now + }), + {:ok, conversation} <- + create_conversation(%{ + account_id: account_id, + customer_id: customer.id, + source: sms_source + }) do + {:ok, customer, conversation} + else + error -> + error + end + + [customer] -> + with nil <- + find_latest_conversation(account_id, %{ + customer_id: customer.id, + source: sms_source + }), + {:ok, conversation} <- + create_conversation(%{ + account_id: account_id, + customer_id: customer.id, + source: sms_source + }) do + {:ok, customer, conversation} + else + %Conversation{} = conversation -> + {:ok, customer, conversation} + + error -> + error + end + end + end + + + ##################### # Private methods ##################### diff --git a/lib/chat_api/twilio.ex b/lib/chat_api/twilio.ex index 87d5d64cb..3e2bf0131 100644 --- a/lib/chat_api/twilio.ex +++ b/lib/chat_api/twilio.ex @@ -7,8 +7,6 @@ defmodule ChatApi.Twilio do alias ChatApi.Repo alias ChatApi.Twilio.TwilioAuthorization - alias ChatApi.Conversations - alias ChatApi.Customers @spec list_twilio_authorizations() :: [TwilioAuthorization.t()] def list_twilio_authorizations() do @@ -84,57 +82,6 @@ defmodule ChatApi.Twilio do |> Repo.one() end - @spec find_or_create_customer_and_conversation(String.t(), String.t()) :: - {:ok, Customers.Customer.t(), Conversations.Conversation.t()} - | {:error, Ecto.Changeset.t()} - def find_or_create_customer_and_conversation(account_id, phone) do - now = DateTime.utc_now() - sms_source = "sms" - - case Customers.list_customers(account_id, %{phone: phone}) do - [] -> - with {:ok, customer} <- - Customers.create_customer(%{ - phone: phone, - account_id: account_id, - first_seen: now, - last_seen: now - }), - {:ok, conversation} <- - Conversations.create_conversation(%{ - account_id: account_id, - customer_id: customer.id, - source: sms_source - }) do - {:ok, customer, conversation} - else - error -> - error - end - - [customer] -> - with nil <- - Conversations.find_latest_conversation(account_id, %{ - customer_id: customer.id, - source: sms_source - }), - {:ok, conversation} <- - Conversations.create_conversation(%{ - account_id: account_id, - customer_id: customer.id, - source: sms_source - }) do - {:ok, customer, conversation} - else - %Conversations.Conversation{} = conversation -> - {:ok, customer, conversation} - - error -> - error - end - end - end - defp filter_authorizations_where(params) do Enum.reduce(params, dynamic(true), fn {:account_id, value}, dynamic -> diff --git a/lib/chat_api_web/controllers/twilio_controller.ex b/lib/chat_api_web/controllers/twilio_controller.ex index 07c2fd791..017d5df88 100644 --- a/lib/chat_api_web/controllers/twilio_controller.ex +++ b/lib/chat_api_web/controllers/twilio_controller.ex @@ -3,6 +3,7 @@ defmodule ChatApiWeb.TwilioController do alias ChatApi.Messages alias ChatApi.Twilio + alias ChatApi.Conversations alias ChatApi.Twilio.TwilioAuthorization require Logger @@ -73,7 +74,7 @@ defmodule ChatApiWeb.TwilioController do from_phone_number: to }), {:ok, customer, conversation} <- - Twilio.find_or_create_customer_and_conversation(account_id, from), + Conversations.find_or_create_customer_and_conversation(account_id, from), {:ok, _mesage} <- Messages.create_message(%{ body: body, diff --git a/test/chat_api/conversations_test.exs b/test/chat_api/conversations_test.exs index 99227231c..d74661682 100644 --- a/test/chat_api/conversations_test.exs +++ b/test/chat_api/conversations_test.exs @@ -771,6 +771,66 @@ defmodule ChatApi.ConversationsTest do end end + describe "find_or_create_customer_and_conversation/2" do + test "returns customer and new conversation when customer exists" do + account = insert(:account) + customer = insert(:customer, account: account) + + {:ok, found_customer, found_conversation} = + Conversations.find_or_create_customer_and_conversation(account.id, customer.phone) + + assert customer.id == found_customer.id + + assert customer.id == found_conversation.customer_id + assert "sms" == found_conversation.source + end + + test "returns new customer and conversation when customer doesn't exist" do + account = insert(:account) + phone_number = "+18675309" + + {:ok, found_customer, found_conversation} = + Conversations.find_or_create_customer_and_conversation(account.id, phone_number) + + assert phone_number == found_customer.phone + assert account.id == found_customer.account_id + assert account.id == found_conversation.account_id + end + + test "returns latest conversations when multiple conversations exist" do + account = insert(:account) + customer = insert(:customer, account: account) + + insert(:conversation, + account: account, + customer: customer, + inserted_at: ~N[2020-12-01 00:00:00], + source: "sms" + ) + + insert(:conversation, + account: account, + customer: customer, + inserted_at: ~N[2020-12-01 00:01:00], + source: "sms" + ) + + latest_conversation = + insert(:conversation, + account: account, + customer: customer, + inserted_at: ~N[2020-12-01 00:02:00], + source: "sms" + ) + + {:ok, found_customer, found_conversation} = + Conversations.find_or_create_customer_and_conversation(account.id, "+18675309") + + assert latest_conversation.id == found_conversation.id + assert customer.id == found_customer.id + end + end + defp days_ago(days) do DateTime.utc_now() |> DateTime.add(days * 60 * 60 * 24 * -1) diff --git a/test/chat_api/twilio_test.exs b/test/chat_api/twilio_test.exs deleted file mode 100644 index c4220f31c..000000000 --- a/test/chat_api/twilio_test.exs +++ /dev/null @@ -1,65 +0,0 @@ -defmodule ChatApi.TwilioTest do - use ChatApi.DataCase, async: true - import ChatApi.Factory - alias ChatApi.Twilio - - describe "find_or_create_customer_and_conversation/2" do - test "returns customer and new conversation when customer exists" do - account = insert(:account) - customer = insert(:customer, account: account) - - {:ok, found_customer, found_conversation} = - Twilio.find_or_create_customer_and_conversation(account.id, customer.phone) - - assert customer.id == found_customer.id - - assert customer.id == found_conversation.customer_id - assert "sms" == found_conversation.source - end - - test "returns new customer and conversation when customer doesn't exist" do - account = insert(:account) - phone_number = "+18675309" - - {:ok, found_customer, found_conversation} = - Twilio.find_or_create_customer_and_conversation(account.id, phone_number) - - assert phone_number == found_customer.phone - assert account.id == found_customer.account_id - assert account.id == found_conversation.account_id - end - - test "returns latest conversations when multiple conversations exist" do - account = insert(:account) - customer = insert(:customer, account: account) - - insert(:conversation, - account: account, - customer: customer, - inserted_at: ~N[2020-12-01 00:00:00], - source: "sms" - ) - - insert(:conversation, - account: account, - customer: customer, - inserted_at: ~N[2020-12-01 00:01:00], - source: "sms" - ) - - latest_conversation = - insert(:conversation, - account: account, - customer: customer, - inserted_at: ~N[2020-12-01 00:02:00], - source: "sms" - ) - - {:ok, found_customer, found_conversation} = - Twilio.find_or_create_customer_and_conversation(account.id, "+18675309") - - assert latest_conversation.id == found_conversation.id - assert customer.id == found_customer.id - end - end -end diff --git a/test/chat_api_web/controllers/twilio_controller_test.exs b/test/chat_api_web/controllers/twilio_controller_test.exs index cf8b34392..02364cb85 100644 --- a/test/chat_api_web/controllers/twilio_controller_test.exs +++ b/test/chat_api_web/controllers/twilio_controller_test.exs @@ -8,6 +8,7 @@ defmodule ChatApiWeb.TwilioControllerTest do alias ChatApi.Customers.Customer alias ChatApi.Messages alias ChatApi.Messages.Message + alias ChatApi.Conversations alias ChatApi.Conversations.Conversation alias ChatApi.Twilio.TwilioAuthorization @@ -37,7 +38,13 @@ defmodule ChatApiWeb.TwilioControllerTest do [ find_twilio_authorization: fn _ -> %TwilioAuthorization{account_id: 1} - end, + end + ] + }, + { + Conversations, + [], + [ find_or_create_customer_and_conversation: fn _, __ -> {:ok, %Customer{id: 1}, %Conversation{id: 1}} end @@ -81,7 +88,13 @@ defmodule ChatApiWeb.TwilioControllerTest do [ find_twilio_authorization: fn _ -> %TwilioAuthorization{account_id: 1} - end, + end + ] + }, + { + Conversations, + [], + [ find_or_create_customer_and_conversation: fn _, __ -> {:error, %Ecto.Changeset{}} end From b6b5b796cfa8928bfba703814fdc18f573777a08 Mon Sep 17 00:00:00 2001 From: Daven Casia <77761194+fmterrorf@users.noreply.github.com> Date: Mon, 29 Mar 2021 18:48:16 -0600 Subject: [PATCH 10/14] Mix format --- lib/chat_api/conversations.ex | 2 -- 1 file changed, 2 deletions(-) diff --git a/lib/chat_api/conversations.ex b/lib/chat_api/conversations.ex index 2891c3bfc..7a2bbc61f 100644 --- a/lib/chat_api/conversations.ex +++ b/lib/chat_api/conversations.ex @@ -559,8 +559,6 @@ defmodule ChatApi.Conversations do end end - - ##################### # Private methods ##################### From b175d3a2a17409789f1509317d37dd3732cf5ba9 Mon Sep 17 00:00:00 2001 From: Daven Casia <77761194+fmterrorf@users.noreply.github.com> Date: Mon, 29 Mar 2021 18:54:56 -0600 Subject: [PATCH 11/14] Cleanup typespec --- lib/chat_api/conversations.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/chat_api/conversations.ex b/lib/chat_api/conversations.ex index 7a2bbc61f..5769ca380 100644 --- a/lib/chat_api/conversations.ex +++ b/lib/chat_api/conversations.ex @@ -509,7 +509,7 @@ defmodule ChatApi.Conversations do end @spec find_or_create_customer_and_conversation(String.t(), String.t()) :: - {:ok, Customers.Customer.t(), Conversation.t()} + {:ok, Customer.t(), Conversation.t()} | {:error, Ecto.Changeset.t()} def find_or_create_customer_and_conversation(account_id, phone) do now = DateTime.utc_now() From ec50e2cee6a3a6be3fc34a18ae5c5db98570d5a1 Mon Sep 17 00:00:00 2001 From: Daven Casia <77761194+fmterrorf@users.noreply.github.com> Date: Wed, 31 Mar 2021 16:35:30 -0600 Subject: [PATCH 12/14] Log if twilio account is not found --- lib/chat_api_web/controllers/twilio_controller.ex | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/chat_api_web/controllers/twilio_controller.ex b/lib/chat_api_web/controllers/twilio_controller.ex index 017d5df88..7af48a18f 100644 --- a/lib/chat_api_web/controllers/twilio_controller.ex +++ b/lib/chat_api_web/controllers/twilio_controller.ex @@ -85,7 +85,8 @@ defmodule ChatApiWeb.TwilioController do send_resp(conn, 200, "") else nil -> - send_resp(conn, 404, "Twilio account not found") + Logger.info("Twilio account not found") + send_resp(conn, 200, "") error -> Logger.error(inspect(error)) From 1366a2ccc502beb8545ae0834597831d3bc756d4 Mon Sep 17 00:00:00 2001 From: Daven Casia <77761194+fmterrorf@users.noreply.github.com> Date: Wed, 31 Mar 2021 16:35:53 -0600 Subject: [PATCH 13/14] Log warn message if twilio account is not found --- lib/chat_api_web/controllers/twilio_controller.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/chat_api_web/controllers/twilio_controller.ex b/lib/chat_api_web/controllers/twilio_controller.ex index 7af48a18f..db3cad464 100644 --- a/lib/chat_api_web/controllers/twilio_controller.ex +++ b/lib/chat_api_web/controllers/twilio_controller.ex @@ -85,7 +85,7 @@ defmodule ChatApiWeb.TwilioController do send_resp(conn, 200, "") else nil -> - Logger.info("Twilio account not found") + Logger.warn("Twilio account not found") send_resp(conn, 200, "") error -> From 1a0e010d6e1701b9e9d595a715a30fc739665828 Mon Sep 17 00:00:00 2001 From: Daven Casia <77761194+fmterrorf@users.noreply.github.com> Date: Wed, 31 Mar 2021 16:43:01 -0600 Subject: [PATCH 14/14] Fix twilio controller test --- test/chat_api_web/controllers/twilio_controller_test.exs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/chat_api_web/controllers/twilio_controller_test.exs b/test/chat_api_web/controllers/twilio_controller_test.exs index 02364cb85..82da2ca28 100644 --- a/test/chat_api_web/controllers/twilio_controller_test.exs +++ b/test/chat_api_web/controllers/twilio_controller_test.exs @@ -62,7 +62,7 @@ defmodule ChatApiWeb.TwilioControllerTest do end end - test "returns 404 when twilio account is not found", + test "returns 200 when twilio account is not found", %{authed_conn: authed_conn} do with_mocks([ { @@ -75,7 +75,7 @@ defmodule ChatApiWeb.TwilioControllerTest do ]) do conn = post(authed_conn, Routes.twilio_path(authed_conn, :webhook), @request_body) - assert response(conn, 404) + assert response(conn, 200) end end