Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ by adding `flow_runner` to your list of dependencies in `mix.exs`:
```elixir
def deps do
[
{:flow_runner, "~> 6.14.1"}
{:flow_runner, "~> 6.15.0"}
]
end
```
Expand Down
75 changes: 31 additions & 44 deletions lib/flow_runner/custom_blocks/whatsapp_template_message.ex
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,9 @@ defmodule FlowRunner.CustomBlocks.WhatsAppTemplateMessage do
send_message_template("template_name", "en", ["param-1", "param-2"])
end
""",
returns: "Map with __value__ and index when template has reply buttons"
returns:
"The submitted JSON when the template has a flow button, otherwise a map " <>
"with __value__ and index when the template has reply buttons"
@impl FlowRunner.Spec.Block
def validate_config!(%{
"template" =>
Expand Down Expand Up @@ -68,6 +70,7 @@ defmodule FlowRunner.CustomBlocks.WhatsAppTemplateMessage do
required(:type) => String.t(),
optional(:text) => String.t(),
optional(:payload) => String.t(),
optional(:flow_action_data) => String.t(),
optional(:language) => String.t()
})
}
Expand All @@ -80,63 +83,39 @@ defmodule FlowRunner.CustomBlocks.WhatsAppTemplateMessage do
index: component["index"],
# required only for buttons
sub_type: component["sub_type"],
parameters: Enum.map(parameters, &parse_parameter(&1, default_language))
parameters:
Enum.map(parameters, fn component ->
language = component["language"] || Expression.evaluate_block!(default_language)

Map.put(parse_parameter(component), :language, language)
end)
}

defp parse_parameter(%{"type" => "text", "text" => text} = component, default_language),
do: %{
type: "text",
text: text,
language: component["language"] || Expression.evaluate_block!(default_language)
}
defp parse_parameter(%{"type" => "text", "text" => text}),
do: %{type: "text", text: text}

defp parse_parameter(
%{"type" => "document", "document" => %{"link" => link} = document},
default_language
) do
defp parse_parameter(%{"type" => "document", "document" => %{"link" => link} = document}) do
document =
if filename = document["filename"] do
%{link: link, filename: filename}
else
%{link: link}
end

%{
type: "document",
document: document,
language: document["language"] || Expression.evaluate_block!(default_language)
}
%{type: "document", document: document}
end

defp parse_parameter(
%{"type" => "video", "video" => %{"link" => link}} = video,
default_language
),
do: %{
type: "video",
video: %{link: link},
language: video["language"] || Expression.evaluate_block!(default_language)
}
defp parse_parameter(%{"type" => "video", "video" => %{"link" => link}}),
do: %{type: "video", video: %{link: link}}

defp parse_parameter(
%{"type" => "image", "image" => %{"link" => link}} = image,
default_language
),
do: %{
type: "image",
image: %{link: link},
language: image["language"] || Expression.evaluate_block!(default_language)
}
defp parse_parameter(%{"type" => "image", "image" => %{"link" => link}}),
do: %{type: "image", image: %{link: link}}

defp parse_parameter(
%{"type" => "payload", "payload" => payload_resource_uuid} = payload,
default_language
),
do: %{
type: "payload",
payload: payload_resource_uuid,
language: payload["language"] || Expression.evaluate_block!(default_language)
}
defp parse_parameter(%{"type" => "payload", "payload" => payload_resource_uuid}),
do: %{type: "payload", payload: payload_resource_uuid}

defp parse_parameter(%{"type" => "action", "flow_action_data" => flow_action_data}),
do: %{type: "action", flow_action_data: flow_action_data}

@impl FlowRunner.Spec.Block
@decorate with_span("DSL.Blocks.WhatsAppTemplateMessage.evaluate_incoming")
Expand All @@ -154,6 +133,14 @@ defmodule FlowRunner.CustomBlocks.WhatsAppTemplateMessage do
@impl FlowRunner.Spec.Block
def evaluate_outgoing(_container, _flow, _block, _context, nil), do: {:ok, nil}

# A WhatsApp Flow submission arrives as a decoded JSON reply, which always
# carries the `flow_token` echoed from the outbound message. Return it unwrapped so
# the submitted fields land directly on the block's var (`@ref_Template_1.email`),
# matching how `Io.Turn.WhatsAppSendFlow` exposes its result.
def evaluate_outgoing(_container, _flow, _block, _context, %{"flow_token" => _} = flow_response) do
{:ok, flow_response}
end

@template_button_indices Enum.map(0..9, &to_string/1)
def evaluate_outgoing(_container, _flow, block, _context, "template-btn-idx-" <> index)
when index in @template_button_indices do
Expand Down
2 changes: 1 addition & 1 deletion mix.exs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
defmodule FlowRunner.MixProject do
use Mix.Project

@version "6.14.1"
@version "6.15.0"

def project do
[
Expand Down
200 changes: 200 additions & 0 deletions test/flow_runner/custom_blocks/whatsapp_template_message_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,23 @@ defmodule FlowRunner.CustomBlocks.WhatsAppTemplateMessageTest do
use ExUnit.Case, async: true

alias FlowRunner.CustomBlocks.WhatsAppTemplateMessage
alias FlowRunner.Spec.Block
alias FlowRunner.Spec.Container
alias FlowRunner.Spec.Exit
alias FlowRunner.Spec.Flow

defp block_with_components(components, exits \\ []) do
%Block{
uuid: "block-uuid",
name: "ref_Template_1",
type: "Io.Turn.WhatsAppTemplateMessage",
config: %{template: %{components: components}},
exits: exits
}
end

defp button_component(sub_type),
do: %{type: "button", sub_type: sub_type, index: "0", parameters: []}

describe "validate_config!/1 for document parameter" do
test "returns document with filename when present" do
Expand Down Expand Up @@ -145,4 +162,187 @@ defmodule FlowRunner.CustomBlocks.WhatsAppTemplateMessageTest do
assert param.language == {:not_found, ["en"]}
end
end

describe "validate_config!/1 for a flow button" do
test "carries the flow_action_data payload through untouched" do
config = %{
"template" => %{
"name" => "foo",
"language" => %{"code" => "en"},
"components" => [
%{
"type" => "button",
"sub_type" => "flow",
"index" => "0",
"parameters" => [
%{"type" => "action", "flow_action_data" => %{"patient_id" => "42"}}
]
}
]
}
}

result = WhatsAppTemplateMessage.validate_config!(config)

assert [%{type: "button", sub_type: "flow", index: "0", parameters: [param]}] =
result.template.components

assert param.type == "action"
assert param.flow_action_data == %{"patient_id" => "42"}
end

test "keeps an unevaluated expression payload as a string" do
config = %{
"template" => %{
"name" => "foo",
"language" => %{"code" => "en"},
"components" => [
%{
"type" => "button",
"sub_type" => "flow",
"index" => "0",
"parameters" => [%{"type" => "action", "flow_action_data" => "@some_var"}]
}
]
}
}

result = WhatsAppTemplateMessage.validate_config!(config)
[%{parameters: [param]}] = result.template.components
assert param.flow_action_data == "@some_var"
end
end

describe "evaluate_incoming/4" do
test "waits for user input when the template has a flow button" do
block = block_with_components([%{type: "body"}, button_component("flow")])

assert {:ok, _container, _flow, _block, %{waiting_for_user_input: true}} =
WhatsAppTemplateMessage.evaluate_incoming(
%Container{},
%Flow{},
block,
%FlowRunner.Context{}
)
end

test "waits for user input when the template has a quick reply button" do
block = block_with_components([%{type: "body"}, button_component("quick_reply")])

assert {:ok, _container, _flow, _block, %{waiting_for_user_input: true}} =
WhatsAppTemplateMessage.evaluate_incoming(
%Container{},
%Flow{},
block,
%FlowRunner.Context{}
)
end

test "does not wait when the template only has url buttons" do
block = block_with_components([%{type: "body"}, button_component("url")])

assert {:ok, _container, _flow, _block, %{waiting_for_user_input: false}} =
WhatsAppTemplateMessage.evaluate_incoming(
%Container{},
%Flow{},
block,
%FlowRunner.Context{}
)
end

test "does not wait when the template has no buttons" do
block = block_with_components([%{type: "body"}])

assert {:ok, _container, _flow, _block, %{waiting_for_user_input: false}} =
WhatsAppTemplateMessage.evaluate_incoming(
%Container{},
%Flow{},
block,
%FlowRunner.Context{}
)
end
end

describe "evaluate_outgoing/5 for a flow submission" do
test "returns the submitted json unwrapped so fields land on the block var" do
block = block_with_components([%{type: "body"}, button_component("flow")])

flow_response = %{
"flow_token" => "a-flow-token",
"email" => "jane@example.com",
"slot" => "09:30"
}

assert {:ok, ^flow_response} =
WhatsAppTemplateMessage.evaluate_outgoing(
%Container{},
%Flow{},
block,
%FlowRunner.Context{},
flow_response
)
end

test "wraps a plain text reply so a non-submission still routes to the fallback exit" do
block = block_with_components([%{type: "body"}, button_component("flow")])

assert {:ok, %{"__value__" => "no thanks", "index" => nil}} =
WhatsAppTemplateMessage.evaluate_outgoing(
%Container{},
%Flow{},
block,
%FlowRunner.Context{},
"no thanks"
)
end
end

describe "evaluate_outgoing/5 for button and text input" do
test "returns the prefixed value when the block exits use the prefix" do
block =
block_with_components(
[%{type: "body"}, button_component("quick_reply")],
[%Exit{uuid: "exit-uuid", test: "block.value == \"template-btn-idx-0\""}]
)

assert {:ok, %{"__value__" => "template-btn-idx-0", "index" => "0"}} =
WhatsAppTemplateMessage.evaluate_outgoing(
%Container{},
%Flow{},
block,
%FlowRunner.Context{},
"template-btn-idx-0"
)
end

test "returns the bare index for legacy blocks whose exits have no prefix" do
block =
block_with_components(
[%{type: "body"}, button_component("quick_reply")],
[%Exit{uuid: "exit-uuid", test: "block.value == \"0\""}]
)

assert {:ok, %{"__value__" => "0", "index" => "0"}} =
WhatsAppTemplateMessage.evaluate_outgoing(
%Container{},
%Flow{},
block,
%FlowRunner.Context{},
"template-btn-idx-0"
)
end

test "passes nil through untouched" do
block = block_with_components([%{type: "body"}])

assert {:ok, nil} =
WhatsAppTemplateMessage.evaluate_outgoing(
%Container{},
%Flow{},
block,
%FlowRunner.Context{},
nil
)
end
end
end
Loading