Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Filter push messages #25

Merged
merged 8 commits into from
Oct 29, 2024
Merged
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
6 changes: 3 additions & 3 deletions .github/workflows/test.yaml
Original file line number Diff line number Diff line change
@@ -20,10 +20,10 @@ jobs:
uses: actions/checkout@v4
with:
repository: praekeltfoundation/flow_tester
ref: v0.3.5
ref: v0.3.6
path: flow_tester
ssh-key: ${{ secrets.SSH_PRIVATE_KEY }}
- name: Check formatting
run: mix format --check-formatted "Browsable FAQs/QA/tests/*.exs"
run: mix format --check-formatted "Browsable FAQs/QA/tests/*.exs" "Push messaging/QA/tests/*.exs"
- name: Test flows
run: ./flow_tester/run_flow_tests.exs "Browsable FAQs/QA/tests/"
run: ./flow_tester/run_flow_tests.exs "Browsable FAQs/QA/tests/" "Push messaging/QA/tests/"
1 change: 0 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -113,7 +113,6 @@ GitHub.sublime-settings

# Visual Studio Code #
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
144 changes: 144 additions & 0 deletions Push messaging/QA/flows/send_next_message.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
```stack
# One trigger for each message that needs to be sent in OCS
trigger(interval: "+5m", relative_to: "contact.push_messaging_signup")
trigger(interval: "+10m", relative_to: "contact.push_messaging_signup")
```

This stack fetches the message to be sent based on the difference between the current time and the signup time.

It handles the following message types:

* Template messages. Assumes template message with two variables. Sets the first to the contact's whatsapp profile name, and the second to the literal "Second"
* Text messages. Sends the title, followed by the body, of the whatsapp message, in a single message to the user.

It then increments the content set position on the contact, and runs the stack that handles scheduling the next message in the message set.

## Configuration

This Journey requires the `config.contentrepo_token` global variable to be set.

This Journey also requires configuration for "gender", "age", and/or "relationship" which is used to fetch the correct Ordered Content Set.

## Contact fields

* push_messaging_signup, the time the user signed up for these messages. The name of this contact field should be changed according to the implementation, otherwise all the push messages will overwrite each other's scheduled times.
* whatsapp_profile_name, used to personalise the template sent to the user

## Flow results

* template_sent, which message template was sent
* message_sent, the message sent to the user

## Connections to other stacks

This Journey does not link to any other Journeys

## Determine message

This block figures out which message to send to the user based on the difference between the current time, and when the user signed up plus the trigger time. This way if we update CMS with a new message then the correct sequence is still followed for users partway through.

```stack
card DetermineMessage
when now() >= datetime_add(contact.push_messaging_signup, 5, "m") and
now() < datetime_add(contact.push_messaging_signup, 10, "m"),
then: CalculateAge do
# send first message
push_messaging_content_set_position = 0
end
# Add your other conditions here
Copy link

@fritzbrand fritzbrand Oct 28, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this an old comment that can be removed, or one we should keep there for future dev?

card DetermineMessage, then: CalculateAge do
# send second message
push_messaging_content_set_position = 1
end
```

## Calculate Age & Determine Age Range

CMS uses age ranges, so in order to filter by age, we need get the age either from the `age` contact field or calculate it from the `year_of_birth` contact field, and then determine which age range it falls into.

```stack
card CalculateAge, then: DetermineAgeRange do
# age =
# if is_nil_or_empty(contact.age) do
# year(now()) - contact.year_of_birth
# else
# contact.age
# end
# we don't currently have an age contact field and it seems silly to have to create it for
# this journey
age =
if is_nil_or_empty(contact.year_of_birth) do
0
else
year(now()) - contact.year_of_birth
end
end
card DetermineAgeRange when age >= 15 and age <= 18, then: GetMessage do
age_range = "15 - 18"
end
card DetermineAgeRange when age >= 25 and age <= 30, then: GetMessage do
age_range = "25 - 30"
end
card DetermineAgeRange, then: GetMessage do
age_range = ""
end
```

```stack
card GetMessage, then: SendMessage do
contentsets =
get(
"https://content-repo-api-qa.prk-k8s.prd-p6t.org/api/v2/orderedcontent/",
headers: [
["Authorization", "Token @global.config.contentrepo_token"]
],
query: [
["gender", "@contact.gender"],
["age", "@age_range"],
["relationship", "@contact.relationship_status"]
]
)
contentset = contentsets.body.results[0]
contentset_item = contentset.pages[push_messaging_content_set_position]
page =
get(
"https://content-repo-api-qa.prk-k8s.prd-p6t.org/api/v2/pages/@contentset_item.id/",
headers: [
["Authorization", "Token @global.config.contentrepo_token"]
],
query: [["whatsapp", "true"]]
)
end
card SendMessage when page.body.body.is_whatsapp_template do
write_result("template_sent", "@page.body.body.whatsapp_template_name")
send_message_template("@page.body.body.whatsapp_template_name", "en_US", [
"@contact.whatsapp_profile_name",
"Second"
])
end
card SendMessage do
write_result("message_sent", "@page.body.id")
text("""
@page.body.title
@page.body.body.text.value.message
""")
end
```
69 changes: 69 additions & 0 deletions Push messaging/QA/flows/signup.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
# Push Messaging: Signup

This flow asks the user whether they want to sign up for the testing push messaging.

If they decline, then we exit

If they accept, then we search the ordered content sets on contentrepo for the one with the profile fields specified in the config.

## Configuration

This Journey requires the `config.contentrepo_token` global variable to be set.

This Journey also requires configuration for "gender", "age", and/or "relationship" which is used to fetch the correct Ordered Content Set.

## Contact fields

* push_messaging_signup, the time when they signed up for push messages

## Flow results

* push_messaging_signup, the id of the content set that the user signed up for, or no if they didn't sign up

## Connections to other stacks

This Journey does not link to any other Journeys

```stack
card AskSignup do
buttons(FetchContentSet: "Yes, please", Exit: "No, thank you") do
text("""
Hi!
Would you like to sign up to our testing messaging set?
You will receive 5 messages, one every 5 minutes.
""")
end
end
card FetchContentSet, then: CompleteSignup do
contentsets =
get(
"https://content-repo-api-qa.prk-k8s.prd-p6t.org/api/v2/orderedcontent/",
headers: [
["Authorization", "Token @global.config.contentrepo_token"]
],
query: [
["gender", "@contact.gender"],
["age", "@contact.age"],
["relationship", "@contact.relationship"]
]
)
contentset = contentsets.body.results[0]
end
card CompleteSignup do
signup_time = now()
update_contact(push_messaging_signup: "@signup_time")
write_result("push_messaging_signup", "@contentset.id")
text("Thank you for signing up! You will receive your first message shortly")
end
card Exit do
write_result("push_messaging_signup", "no")
text("Thank you! You will not be sent any messages.")
end
```
1 change: 1 addition & 0 deletions Push messaging/QA/flows_json/send_next_message.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"name":"pm-send-next-message","description":"Default description","uuid":"55b1690d-a546-4d33-912c-7f2b83665ef0","resources":[{"values":[{"value":"@page.body.title\n\n@page.body.body.text.value.message\n","modes":["RICH_MESSAGING"],"content_type":"TEXT","mime_type":"text/plain","language_id":"248f684f-005e-49e8-a567-072ea71d3a8a"}],"uuid":"085fb255-222f-44e4-82e1-baa654437922"}],"flows":[{"label":null,"name":"stack","blocks":[{"label":null,"name":"send_message_case","type":"Core.Case","config":{},"tags":[],"uuid":"4a33bb3c-ce05-5ffc-a22a-aedae889d235","ui_metadata":{},"exits":[{"default":false,"name":"Exit for send_message_case_condition_0","config":{},"test":"page.body.body.is_whatsapp_template","uuid":"3066c9b1-294a-4e1a-80e9-cd5a7328ca9c","destination_block":"bd67ea29-74a9-57db-b900-1beac600c99d","semantic_label":null,"vendor_metadata":{}},{"default":true,"name":"Exit for send_message_case_condition_1","config":{},"test":null,"uuid":"15c36c7a-13f8-4dc8-8265-5356e7989303","destination_block":"0653cac5-4125-50a2-b335-1c1a9c9439c4","semantic_label":null,"vendor_metadata":{}}],"semantic_label":null,"vendor_metadata":{}},{"label":null,"name":"message_sent","type":"Core.Output","config":{"value":"\"@page.body.id\""},"tags":[],"uuid":"0653cac5-4125-50a2-b335-1c1a9c9439c4","ui_metadata":{"canvas_coordinates":{"x":0,"y":0}},"exits":[{"default":true,"name":"message_sent","config":{},"test":"","uuid":"909777bc-0bb7-41ad-ac4b-fb05243dbdd0","destination_block":"8b939f5b-ac37-5a69-b67d-a160209775a8","semantic_label":"","vendor_metadata":{}}],"semantic_label":null,"vendor_metadata":{"io":{"turn":{"stacks_dsl":{"0.1.0":{"card":{"condition":null,"meta":{"column":1,"line":87},"name":"SendMessage","uuid":"b2c3e2ca-ba63-5954-bcc6-346d54f6e74e"},"card_item":{"meta":{"column":3,"line":88},"type":"write_result","write_result":{}},"index":1}}}}}},{"label":null,"name":"send_message_case_condition_1_text","type":"MobilePrimitives.Message","config":{"prompt":"085fb255-222f-44e4-82e1-baa654437922"},"tags":[],"uuid":"8b939f5b-ac37-5a69-b67d-a160209775a8","ui_metadata":{"canvas_coordinates":{"x":0,"y":0}},"exits":[{"default":true,"name":"send_message_case_condition_1_text","config":{},"test":"","uuid":"7401eff7-3221-4e00-9b19-9b37d63b8acd","destination_block":null,"semantic_label":"","vendor_metadata":{}}],"semantic_label":null,"vendor_metadata":{"io":{"turn":{"stacks_dsl":{"0.1.0":{"card":{"condition":null,"meta":{"column":1,"line":87},"name":"SendMessage","uuid":"b2c3e2ca-ba63-5954-bcc6-346d54f6e74e"},"card_item":{"meta":{"column":3,"line":90},"text":{},"type":"text"},"index":0}}}}}},{"label":null,"name":"template_sent","type":"Core.Output","config":{"value":"\"@page.body.body.whatsapp_template_name\""},"tags":[],"uuid":"bd67ea29-74a9-57db-b900-1beac600c99d","ui_metadata":{"canvas_coordinates":{"x":0,"y":0}},"exits":[{"default":true,"name":"template_sent","config":{},"test":"","uuid":"73eb97ad-ddd2-49a8-aa70-4565256717de","destination_block":"b295e1ff-ce5e-51db-9528-16246c1ed8fa","semantic_label":"","vendor_metadata":{}}],"semantic_label":null,"vendor_metadata":{"io":{"turn":{"stacks_dsl":{"0.1.0":{"card":{"condition":"page.body.body.is_whatsapp_template","meta":{"column":1,"line":78},"name":"SendMessage","uuid":"4a33bb3c-ce05-5ffc-a22a-aedae889d235"},"card_item":{"meta":{"column":3,"line":79},"type":"write_result","write_result":{}},"index":0}}}}}},{"label":null,"name":"send_message_case_condition_0_whatsapp_template_message","type":"Io.Turn.WhatsAppTemplateMessage","config":{"template":{"name":"\"@page.body.body.whatsapp_template_name\"","components":[{"index":null,"type":"body","parameters":[{"type":"text","text":"\"@contact.whatsapp_profile_name\""},{"type":"text","text":"\"Second\""}],"sub_type":null}],"language":{"code":"\"en_US\""},"tracking":null}},"tags":[],"uuid":"b295e1ff-ce5e-51db-9528-16246c1ed8fa","ui_metadata":{"canvas_coordinates":{"x":0,"y":0}},"exits":[{"default":true,"name":"send_message_case_condition_0_whatsapp_template_message","config":{},"test":"","uuid":"cfc8e9ee-1cf7-4e00-b9d0-b8d797dcec7c","destination_block":null,"semantic_label":"","vendor_metadata":{}}],"semantic_label":null,"vendor_metadata":{"io":{"turn":{"stacks_dsl":{"0.1.0":{"card":{"condition":"page.body.body.is_whatsapp_template","meta":{"column":1,"line":78},"name":"SendMessage","uuid":"4a33bb3c-ce05-5ffc-a22a-aedae889d235"},"card_item":{"meta":{"column":3,"line":81},"type":"whatsapp_template_message","whatsapp_template_message":{}},"index":0}}}}}},{"label":null,"name":"contentsets","type":"Io.Turn.Webhook","config":{"timeout":5000,"mode":"sync","body":null,"query":[["gender","@contact.gender"],["age","@age_range"],["relationship","@contact.relationship_status"]],"url":"https://content-repo-api-qa.prk-k8s.prd-p6t.org/api/v2/orderedcontent/","headers":[["Authorization","Token @global.config.contentrepo_token"]],"method":"GET","cache_ttl":60000},"tags":[],"uuid":"f85a3051-65de-50ae-b66f-88435574da02","ui_metadata":{"canvas_coordinates":{"x":0,"y":0}},"exits":[{"default":true,"name":"contentsets","config":{},"test":"","uuid":"55e9ce5c-4666-4aa0-a752-2ece27f7f471","destination_block":"b3d4494d-6566-55de-b7b1-3c9e3f9c190a","semantic_label":"","vendor_metadata":{}}],"semantic_label":null,"vendor_metadata":{"io":{"turn":{"stacks_dsl":{"0.1.0":{"card":{"condition":null,"meta":{"column":1,"line":50},"name":"GetMessage","uuid":"6f4f7f0b-c111-5248-8be2-104e696b4eeb"},"card_item":{"meta":{"column":3,"line":51},"type":"webhook","webhook":{}},"index":0}}}}}},{"label":null,"name":"contentset","type":"Core.Case","config":{},"tags":[],"uuid":"b3d4494d-6566-55de-b7b1-3c9e3f9c190a","ui_metadata":{"canvas_coordinates":{"x":0,"y":0}},"exits":[{"default":true,"name":"contentsets.body.results[0]","config":{},"test":"","uuid":"7524505e-da97-4650-b35c-ed132c099c06","destination_block":"6f89661d-0a3f-5bea-849e-2aaf3475d136","semantic_label":"","vendor_metadata":{}}],"semantic_label":null,"vendor_metadata":{"io":{"turn":{"stacks_dsl":{"0.1.0":{"card":{"condition":null,"meta":{"column":1,"line":50},"name":"GetMessage","uuid":"6f4f7f0b-c111-5248-8be2-104e696b4eeb"},"card_item":{"expression":{},"meta":{"column":3,"line":64},"type":"expression"},"index":0}}}}}},{"label":null,"name":"contentset_item","type":"Core.Case","config":{},"tags":[],"uuid":"6f89661d-0a3f-5bea-849e-2aaf3475d136","ui_metadata":{"canvas_coordinates":{"x":0,"y":0}},"exits":[{"default":true,"name":"contentset.pages[push_messaging_content_set_position]","config":{},"test":"","uuid":"28af9ccd-0edc-4499-a9ff-eee30177a1cf","destination_block":"c4968226-2322-5be4-9f4d-883bf37c5004","semantic_label":"","vendor_metadata":{}}],"semantic_label":null,"vendor_metadata":{"io":{"turn":{"stacks_dsl":{"0.1.0":{"card":{"condition":null,"meta":{"column":1,"line":50},"name":"GetMessage","uuid":"6f4f7f0b-c111-5248-8be2-104e696b4eeb"},"card_item":{"expression":{},"meta":{"column":3,"line":66},"type":"expression"},"index":0}}}}}},{"label":null,"name":"page","type":"Io.Turn.Webhook","config":{"timeout":5000,"mode":"sync","body":null,"query":[["whatsapp","true"]],"url":"https://content-repo-api-qa.prk-k8s.prd-p6t.org/api/v2/pages/@contentset_item.id/","headers":[["Authorization","Token @global.config.contentrepo_token"]],"method":"GET","cache_ttl":60000},"tags":[],"uuid":"c4968226-2322-5be4-9f4d-883bf37c5004","ui_metadata":{"canvas_coordinates":{"x":0,"y":0}},"exits":[{"default":true,"name":"Default exit to \"SendMessage\"","config":{},"test":"","uuid":"55ab551f-20a1-43d5-8d54-cd75451c25d7","destination_block":"4a33bb3c-ce05-5ffc-a22a-aedae889d235","semantic_label":"","vendor_metadata":{}}],"semantic_label":null,"vendor_metadata":{"io":{"turn":{"stacks_dsl":{"0.1.0":{"card":{"condition":null,"meta":{"column":1,"line":50},"name":"GetMessage","uuid":"6f4f7f0b-c111-5248-8be2-104e696b4eeb"},"card_item":{"meta":{"column":3,"line":68},"type":"webhook","webhook":{}},"index":0}}}}}},{"label":null,"name":"determine_age_range_case","type":"Core.Case","config":{},"tags":[],"uuid":"4693f64a-a63a-533d-be3a-ed5d3bf3b09a","ui_metadata":{},"exits":[{"default":false,"name":"Exit for determine_age_range_case_condition_0","config":{},"test":"and(age >= 15, age <= 18)","uuid":"36b1cfca-9540-47fc-8661-d9b4ff5ced8d","destination_block":"9a65ffbb-f616-59a0-a519-0ec008fcc5a7","semantic_label":null,"vendor_metadata":{}},{"default":false,"name":"Exit for determine_age_range_case_condition_1","config":{},"test":"and(age >= 25, age <= 30)","uuid":"69acfc8c-ae13-41f0-beae-5c36f6207779","destination_block":"bfaacd08-57b5-57a0-a215-5d65553f2bc3","semantic_label":null,"vendor_metadata":{}},{"default":true,"name":"Exit for determine_age_range_case_condition_2","config":{},"test":null,"uuid":"d3ebf181-54f5-4d29-9099-c4681263355b","destination_block":"0b45d8e6-4cf8-5158-a770-d331adb4ba3d","semantic_label":null,"vendor_metadata":{}}],"semantic_label":null,"vendor_metadata":{}},{"label":null,"name":"age_range","type":"Core.Case","config":{},"tags":[],"uuid":"0b45d8e6-4cf8-5158-a770-d331adb4ba3d","ui_metadata":{"canvas_coordinates":{"x":0,"y":0}},"exits":[{"default":true,"name":"\"\"","config":{},"test":"","uuid":"aa34615f-0bc8-4fe9-898d-5ff3d92a6580","destination_block":"f85a3051-65de-50ae-b66f-88435574da02","semantic_label":"","vendor_metadata":{}}],"semantic_label":null,"vendor_metadata":{"io":{"turn":{"stacks_dsl":{"0.1.0":{"card":{"condition":null,"meta":{"column":1,"line":46},"name":"DetermineAgeRange","uuid":"0eb1372c-ccc7-54fe-be99-539a4d72659c"},"card_item":{"literal":{},"meta":{"column":3,"line":47},"type":"literal"},"index":2}}}}}},{"label":null,"name":"age_range","type":"Core.Case","config":{},"tags":[],"uuid":"bfaacd08-57b5-57a0-a215-5d65553f2bc3","ui_metadata":{"canvas_coordinates":{"x":0,"y":0}},"exits":[{"default":true,"name":"\"25 - 30\"","config":{},"test":"","uuid":"3a8a60dc-474f-4989-b73d-ecff96ba8679","destination_block":"f85a3051-65de-50ae-b66f-88435574da02","semantic_label":"","vendor_metadata":{}}],"semantic_label":null,"vendor_metadata":{"io":{"turn":{"stacks_dsl":{"0.1.0":{"card":{"condition":"age >= 25 and age <= 30","meta":{"column":1,"line":42},"name":"DetermineAgeRange","uuid":"a5caae72-e322-5ec3-82d1-e395dc3a507c"},"card_item":{"literal":{},"meta":{"column":3,"line":43},"type":"literal"},"index":1}}}}}},{"label":null,"name":"age_range","type":"Core.Case","config":{},"tags":[],"uuid":"9a65ffbb-f616-59a0-a519-0ec008fcc5a7","ui_metadata":{"canvas_coordinates":{"x":0,"y":0}},"exits":[{"default":true,"name":"\"15 - 18\"","config":{},"test":"","uuid":"f0f8380c-f719-4027-b887-1cd64c1a66e8","destination_block":"f85a3051-65de-50ae-b66f-88435574da02","semantic_label":"","vendor_metadata":{}}],"semantic_label":null,"vendor_metadata":{"io":{"turn":{"stacks_dsl":{"0.1.0":{"card":{"condition":"age >= 15 and age <= 18","meta":{"column":1,"line":38},"name":"DetermineAgeRange","uuid":"4693f64a-a63a-533d-be3a-ed5d3bf3b09a"},"card_item":{"literal":{},"meta":{"column":3,"line":39},"type":"literal"},"index":0}}}}}},{"label":null,"name":"age","type":"Core.Case","config":{},"tags":[],"uuid":"48bb0531-d96f-593c-81fb-1ad12d44742e","ui_metadata":{"canvas_coordinates":{"x":0,"y":0}},"exits":[{"default":true,"name":"if(is_nil_or_empty(contact.year_of_birth), 0, year(now()) - contact.year_of_birth)","config":{},"test":"","uuid":"9f1bc2ee-1ae5-46dd-9222-b6847c9ba0b9","destination_block":"4693f64a-a63a-533d-be3a-ed5d3bf3b09a","semantic_label":"","vendor_metadata":{}}],"semantic_label":null,"vendor_metadata":{"io":{"turn":{"stacks_dsl":{"0.1.0":{"card":{"condition":null,"meta":{"column":1,"line":21},"name":"CalculateAge","uuid":"c26d6e5f-ae37-5937-ba8d-bc9408d944b2"},"card_item":{"expression":{},"meta":{"column":3,"line":30},"type":"expression"},"index":0}}}}}},{"label":null,"name":"determine_message_case","type":"Core.Case","config":{},"tags":[],"uuid":"bf5bcec3-e178-5b01-b978-15752029ab5a","ui_metadata":{},"exits":[{"default":false,"name":"Exit for determine_message_case_condition_0","config":{},"test":"and(now() >= datetime_add(contact.push_messaging_signup, 5, \"m\"), now() < datetime_add(contact.push_messaging_signup, 10, \"m\"))","uuid":"742c9ca0-9a2f-4e63-b02b-b24a126d8f47","destination_block":"b7a96e2c-5091-5f8b-8baa-b25f461d5c0c","semantic_label":null,"vendor_metadata":{}},{"default":true,"name":"Exit for determine_message_case_condition_1","config":{},"test":null,"uuid":"64b2aca4-16d9-49ee-9f51-f7566b9f9eae","destination_block":"0bc54330-8f14-52f5-a7a6-d232c81e6ba4","semantic_label":null,"vendor_metadata":{}}],"semantic_label":null,"vendor_metadata":{}},{"label":null,"name":"push_messaging_content_set_position","type":"Core.Case","config":{},"tags":[],"uuid":"0bc54330-8f14-52f5-a7a6-d232c81e6ba4","ui_metadata":{"canvas_coordinates":{"x":0,"y":0}},"exits":[{"default":true,"name":"1","config":{},"test":"","uuid":"bf4ed9d5-bacb-4725-b0db-7cdcf619b56a","destination_block":"48bb0531-d96f-593c-81fb-1ad12d44742e","semantic_label":"","vendor_metadata":{}}],"semantic_label":null,"vendor_metadata":{"io":{"turn":{"stacks_dsl":{"0.1.0":{"card":{"condition":null,"meta":{"column":1,"line":16},"name":"DetermineMessage","uuid":"59296584-a983-57eb-ad8e-77b3bc994dee"},"card_item":{"literal":{},"meta":{"column":3,"line":18},"type":"literal"},"index":1}}}}}},{"label":null,"name":"push_messaging_content_set_position","type":"Core.Case","config":{},"tags":[],"uuid":"b7a96e2c-5091-5f8b-8baa-b25f461d5c0c","ui_metadata":{"canvas_coordinates":{"x":0,"y":0}},"exits":[{"default":true,"name":"0","config":{},"test":"","uuid":"c5b15e3f-fc9c-4a5e-8680-d393932c8e26","destination_block":"48bb0531-d96f-593c-81fb-1ad12d44742e","semantic_label":"","vendor_metadata":{}}],"semantic_label":null,"vendor_metadata":{"io":{"turn":{"stacks_dsl":{"0.1.0":{"card":{"condition":"now() >= datetime_add(contact.push_messaging_signup, 5, \"m\") and now() < datetime_add(contact.push_messaging_signup, 10, \"m\")","meta":{"column":1,"line":6},"name":"DetermineMessage","uuid":"bf5bcec3-e178-5b01-b978-15752029ab5a"},"card_item":{"literal":{},"meta":{"column":3,"line":11},"type":"literal"},"index":0}}}}}}],"last_modified":"2024-10-28T13:24:32.891241Z","uuid":"cd151682-0f27-58a0-a7b3-281620f3513d","languages":[{"id":"248f684f-005e-49e8-a567-072ea71d3a8a","label":"English","variant":null,"iso_639_3":"eng","bcp_47":null}],"first_block_id":"bf5bcec3-e178-5b01-b978-15752029ab5a","interaction_timeout":300,"vendor_metadata":{},"supported_modes":["RICH_MESSAGING"],"exit_block_id":""}],"vendor_metadata":{},"specification_version":"1.0.0-rc3"}
1 change: 1 addition & 0 deletions Push messaging/QA/flows_json/signup.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"name":"pm-signup","description":"Default description","uuid":"ec6311dc-6447-4701-9c35-7e4a26ec6c3f","resources":[{"values":[{"value":"Thank you for signing up! You will receive your first message shortly","modes":["RICH_MESSAGING"],"content_type":"TEXT","mime_type":"text/plain","language_id":"49445b03-8237-478f-84e2-db5fc5da044b"}],"uuid":"5e3dfb20-c214-42be-b2ff-bb17494a3492"},{"values":[{"value":"Hi!\n\nWould you like to sign up to our testing messaging set?\n\nYou will receive 5 messages, one every 5 minutes.\n","modes":["RICH_MESSAGING"],"content_type":"TEXT","mime_type":"text/plain","language_id":"49445b03-8237-478f-84e2-db5fc5da044b"}],"uuid":"12739269-dd56-453f-8692-f549e68b69b8"},{"values":[{"value":"Yes, please","modes":["RICH_MESSAGING"],"content_type":"TEXT","mime_type":"text/plain","language_id":"49445b03-8237-478f-84e2-db5fc5da044b"}],"uuid":"5360a871-fd32-44b7-8cca-5c6c9400603b"},{"values":[{"value":"No, thank you","modes":["RICH_MESSAGING"],"content_type":"TEXT","mime_type":"text/plain","language_id":"49445b03-8237-478f-84e2-db5fc5da044b"}],"uuid":"5a3526af-b789-46e6-b550-a7cd49b1fffd"},{"values":[{"value":"Thank you! You will not be sent any messages.","modes":["RICH_MESSAGING"],"content_type":"TEXT","mime_type":"text/plain","language_id":"49445b03-8237-478f-84e2-db5fc5da044b"}],"uuid":"c1211df1-77b0-4af8-b9cf-9b0d7628f14c"}],"flows":[{"label":null,"name":"stack","blocks":[{"label":null,"name":"signup_time","type":"Core.Case","config":{},"tags":[],"uuid":"8c3a9347-333a-51dd-b02f-38f652dbfd6c","ui_metadata":{"canvas_coordinates":{"x":0,"y":0}},"exits":[{"default":true,"name":"now()","config":{},"test":"","uuid":"9d7b472f-fef1-490e-bc32-105b67e4ec1b","destination_block":"0a30b02f-7ca8-578a-8847-ba76093420f0","semantic_label":"","vendor_metadata":{}}],"semantic_label":null,"vendor_metadata":{"io":{"turn":{"stacks_dsl":{"0.1.0":{"card":{"condition":null,"meta":{"column":1,"line":30},"name":"CompleteSignup","uuid":"452f5493-dd3d-5952-adfb-8b87e90abfe1"},"card_item":{"expression":{},"meta":{"column":3,"line":31},"type":"expression"},"index":0}}}}}},{"label":null,"name":"complete_signup_contact_update_push_messaging_signup","type":"Core.SetContactProperty","config":{"set_contact_property":{"property_key":"push_messaging_signup","property_value":"@signup_time"}},"tags":[],"uuid":"0a30b02f-7ca8-578a-8847-ba76093420f0","ui_metadata":{"canvas_coordinates":{"x":0,"y":0}},"exits":[{"default":true,"name":"complete_signup_contact_update_push_messaging_signup","config":{},"test":"","uuid":"b8856eb0-5228-4d22-99b4-fcc4293a8be7","destination_block":"1cd04018-728c-5838-95fc-997677eabedb","semantic_label":"","vendor_metadata":{}}],"semantic_label":null,"vendor_metadata":{"io":{"turn":{"stacks_dsl":{"0.1.0":{"card":{"condition":null,"meta":{"column":1,"line":30},"name":"CompleteSignup","uuid":"452f5493-dd3d-5952-adfb-8b87e90abfe1"},"card_item":{"meta":{"column":3,"line":32},"type":"update_contact","update_contact":{}},"index":0}}}}}},{"label":null,"name":"push_messaging_signup","type":"Core.Output","config":{"value":"\"@contentset.id\""},"tags":[],"uuid":"1cd04018-728c-5838-95fc-997677eabedb","ui_metadata":{"canvas_coordinates":{"x":0,"y":0}},"exits":[{"default":true,"name":"push_messaging_signup","config":{},"test":"","uuid":"2f06f06e-fcaf-40eb-9e80-dae56207cd06","destination_block":"1474d567-3b5b-5b43-a26a-b0da3a20563e","semantic_label":"","vendor_metadata":{}}],"semantic_label":null,"vendor_metadata":{"io":{"turn":{"stacks_dsl":{"0.1.0":{"card":{"condition":null,"meta":{"column":1,"line":30},"name":"CompleteSignup","uuid":"452f5493-dd3d-5952-adfb-8b87e90abfe1"},"card_item":{"meta":{"column":3,"line":33},"type":"write_result","write_result":{}},"index":0}}}}}},{"label":null,"name":"complete_signup_text","type":"MobilePrimitives.Message","config":{"prompt":"5e3dfb20-c214-42be-b2ff-bb17494a3492"},"tags":[],"uuid":"1474d567-3b5b-5b43-a26a-b0da3a20563e","ui_metadata":{"canvas_coordinates":{"x":0,"y":0}},"exits":[{"default":true,"name":"complete_signup_text","config":{},"test":"","uuid":"d91c5fb3-25ad-4024-8c0b-8a1fc9c3bc52","destination_block":null,"semantic_label":"","vendor_metadata":{}}],"semantic_label":null,"vendor_metadata":{"io":{"turn":{"stacks_dsl":{"0.1.0":{"card":{"condition":null,"meta":{"column":1,"line":30},"name":"CompleteSignup","uuid":"452f5493-dd3d-5952-adfb-8b87e90abfe1"},"card_item":{"meta":{"column":3,"line":34},"text":{},"type":"text"},"index":0}}}}}},{"label":null,"name":"contentsets","type":"Io.Turn.Webhook","config":{"timeout":5000,"mode":"sync","body":null,"query":[["gender","@contact.gender"],["age","@contact.age"],["relationship","@contact.relationship"]],"url":"https://content-repo-api-qa.prk-k8s.prd-p6t.org/api/v2/orderedcontent/","headers":[["Authorization","Token @global.config.contentrepo_token"]],"method":"GET","cache_ttl":60000},"tags":[],"uuid":"8a9d592c-78fe-51c6-b606-6cfe0ae61532","ui_metadata":{"canvas_coordinates":{"x":0,"y":0}},"exits":[{"default":true,"name":"contentsets","config":{},"test":"","uuid":"c22cfb17-0a9d-4062-abdc-f30bc7a6dae4","destination_block":"9b5f468c-de1e-54f0-a04b-db1c3551c16c","semantic_label":"","vendor_metadata":{}}],"semantic_label":null,"vendor_metadata":{"io":{"turn":{"stacks_dsl":{"0.1.0":{"card":{"condition":null,"meta":{"column":1,"line":13},"name":"FetchContentSet","uuid":"92819633-e996-5604-98ba-cba44404a436"},"card_item":{"meta":{"column":3,"line":14},"type":"webhook","webhook":{}},"index":0}}}}}},{"label":null,"name":"contentset","type":"Core.Case","config":{},"tags":[],"uuid":"9b5f468c-de1e-54f0-a04b-db1c3551c16c","ui_metadata":{"canvas_coordinates":{"x":0,"y":0}},"exits":[{"default":true,"name":"contentsets.body.results[0]","config":{},"test":"","uuid":"ecd67d0b-bf92-4034-b0b0-de2cf8036507","destination_block":"8c3a9347-333a-51dd-b02f-38f652dbfd6c","semantic_label":"","vendor_metadata":{}}],"semantic_label":null,"vendor_metadata":{"io":{"turn":{"stacks_dsl":{"0.1.0":{"card":{"condition":null,"meta":{"column":1,"line":13},"name":"FetchContentSet","uuid":"92819633-e996-5604-98ba-cba44404a436"},"card_item":{"expression":{},"meta":{"column":3,"line":27},"type":"expression"},"index":0}}}}}},{"label":null,"name":"ask_signup","type":"MobilePrimitives.SelectOneResponse","config":{"prompt":"12739269-dd56-453f-8692-f549e68b69b8","choices":[{"name":"fetch_content_set","prompt":"5360a871-fd32-44b7-8cca-5c6c9400603b","test":"block.response = \"Yes, please\""},{"name":"exit","prompt":"5a3526af-b789-46e6-b550-a7cd49b1fffd","test":"block.response = \"No, thank you\""}]},"tags":[],"uuid":"a66f9ab6-6df0-5346-8a85-ab464aa255ae","ui_metadata":{"canvas_coordinates":{"x":0,"y":0}},"exits":[{"default":null,"name":"fetch_content_set","config":{},"test":"block.value = \"fetch_content_set\"","uuid":"7972f09a-613d-48a8-afb8-268c63d6b88c","destination_block":"8a9d592c-78fe-51c6-b606-6cfe0ae61532","semantic_label":null,"vendor_metadata":{}},{"default":null,"name":"exit","config":{},"test":"block.value = \"exit\"","uuid":"4071da74-0057-4b1f-a3f1-2eb3153e894a","destination_block":"f73d5e2f-8a8a-5a03-81e7-2d45760098c2","semantic_label":null,"vendor_metadata":{}},{"default":true,"name":"ask_signup","config":{},"test":"","uuid":"742720d5-da76-48ce-8ade-a8c06153b104","destination_block":null,"semantic_label":"","vendor_metadata":{}}],"semantic_label":null,"vendor_metadata":{"io":{"turn":{"stacks_dsl":{"0.1.0":{"buttons_metadata":{},"card":{"condition":null,"meta":{"column":1,"line":1},"name":"AskSignup","uuid":"cfb4f4a6-6310-525f-a473-bc5b6eda1dc2"},"card_item":{"button_block":{},"meta":{"column":3,"line":2},"type":"button_block"},"index":0}}}}}},{"label":null,"name":"push_messaging_signup","type":"Core.Output","config":{"value":"\"no\""},"tags":[],"uuid":"f73d5e2f-8a8a-5a03-81e7-2d45760098c2","ui_metadata":{"canvas_coordinates":{"x":0,"y":0}},"exits":[{"default":true,"name":"push_messaging_signup","config":{},"test":"","uuid":"e6b3acc7-ccec-4571-a1e5-6a953e0ffcd6","destination_block":"10963182-6b29-5516-91cc-d2dde6537555","semantic_label":"","vendor_metadata":{}}],"semantic_label":null,"vendor_metadata":{"io":{"turn":{"stacks_dsl":{"0.1.0":{"card":{"condition":null,"meta":{"column":1,"line":37},"name":"Exit","uuid":"eddf05c3-f6d4-5ba8-ab1a-e317ec59b710"},"card_item":{"meta":{"column":3,"line":38},"type":"write_result","write_result":{}},"index":0}}}}}},{"label":null,"name":"exit_text","type":"MobilePrimitives.Message","config":{"prompt":"c1211df1-77b0-4af8-b9cf-9b0d7628f14c"},"tags":[],"uuid":"10963182-6b29-5516-91cc-d2dde6537555","ui_metadata":{"canvas_coordinates":{"x":0,"y":0}},"exits":[{"default":true,"name":"exit_text","config":{},"test":"","uuid":"f066141a-07ea-45b6-927a-b7f52fa6f078","destination_block":null,"semantic_label":"","vendor_metadata":{}}],"semantic_label":null,"vendor_metadata":{"io":{"turn":{"stacks_dsl":{"0.1.0":{"card":{"condition":null,"meta":{"column":1,"line":37},"name":"Exit","uuid":"eddf05c3-f6d4-5ba8-ab1a-e317ec59b710"},"card_item":{"meta":{"column":3,"line":39},"text":{},"type":"text"},"index":0}}}}}}],"last_modified":"2024-10-17T13:11:03.104919Z","uuid":"6db00ef4-1fbe-572e-aaf5-ca314ca81c79","languages":[{"id":"49445b03-8237-478f-84e2-db5fc5da044b","label":"English","variant":null,"iso_639_3":"eng","bcp_47":null}],"first_block_id":"a66f9ab6-6df0-5346-8a85-ab464aa255ae","interaction_timeout":300,"vendor_metadata":{},"supported_modes":["RICH_MESSAGING"],"exit_block_id":""}],"vendor_metadata":{},"specification_version":"1.0.0-rc3"}
361 changes: 361 additions & 0 deletions Push messaging/QA/tests/send_next_message_test.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,361 @@
defmodule SendNextMessageTest do
use FlowTester.Case
use FakeCMS

alias FlowTester.WebhookHandler, as: WH

defp flow_path(flow_name), do: Path.join([__DIR__, "..", "flows_json", flow_name <> ".json"])

def setup_fake_cms(auth_token) do
# Start the handler.
wh_pid = start_link_supervised!({FakeCMS, %FakeCMS.Config{auth_token: auth_token}})

topic1_index = %Index{slug: "topic-1", title: "Topic 1"}

leaf_page = %ContentPage{
parent: "topic-1",
slug: "leaf-page-1",
title: "Leaf Page 1",
whatsapp_template_name: "test_name",
wa_messages: [
%WAMsg{
message: "Test leaf content page",
buttons: [%Btn.Next{title: "Next"}]
},
%WAMsg{
message: "Last message"
}
]
}

leaf_page2 = %ContentPage{
parent: "topic-1",
slug: "leaf-page-2",
title: "Leaf Page 2",
wa_messages: [
%WAMsg{
message: "Test leaf content page 2",
buttons: [%Btn.Next{title: "Next"}]
},
%WAMsg{
message: "Last message"
}
]
}

leaf_page3 = %ContentPage{
parent: "topic-1",
slug: "leaf-page-3",
title: "Leaf Page 3",
whatsapp_template_name: "test_name3",
wa_messages: [
%WAMsg{
message: "Test leaf content page 3",
buttons: [%Btn.Next{title: "Next"}]
},
%WAMsg{
message: "Last message"
}
]
}

leaf_page4 = %ContentPage{
parent: "topic-1",
slug: "leaf-page-4",
title: "Leaf Page 4",
wa_messages: [
%WAMsg{
message: "Test leaf content page 4",
buttons: [%Btn.Next{title: "Next"}]
},
%WAMsg{
message: "Last message"
}
]
}

leaf_page5 = %ContentPage{
parent: "topic-1",
slug: "leaf-page-5",
title: "Leaf Page 5",
whatsapp_template_name: "test_name5",
wa_messages: [
%WAMsg{
message: "Test leaf content page 5",
buttons: [%Btn.Next{title: "Next"}]
},
%WAMsg{
message: "Last message"
}
]
}

leaf_page6 = %ContentPage{
parent: "topic-1",
slug: "leaf-page-6",
title: "Leaf Page 6",
wa_messages: [
%WAMsg{
message: "Test leaf content page 6",
buttons: [%Btn.Next{title: "Next"}]
},
%WAMsg{
message: "Last message"
}
]
}

leaf_page7 = %ContentPage{
parent: "topic-1",
slug: "leaf-page-7",
title: "Leaf Page 7",
whatsapp_template_name: "test_name7",
wa_messages: [
%WAMsg{
message: "Test leaf content page 7",
buttons: [%Btn.Next{title: "Next"}]
},
%WAMsg{
message: "Last message"
}
]
}

leaf_page8 = %ContentPage{
parent: "topic-1",
slug: "leaf-page-8",
title: "Leaf Page 8",
wa_messages: [
%WAMsg{
message: "Test leaf content page 8",
buttons: [%Btn.Next{title: "Next"}]
},
%WAMsg{
message: "Last message"
}
]
}

ocs1 = %OrderedContentSet{
id: 1,
name: "Test Ordered Content Set",
profile_fields: [%ProfileField{name: "relationship", value: "single"}],
pages: [
%OrderedContentSetPage{
slug: "leaf-page-1",
time: 1,
unit: "day",
before_or_after: "before",
contact_field: "edd"
},
%OrderedContentSetPage{
slug: "leaf-page-2",
time: 5,
unit: "minutes",
before_or_after: "after",
contact_field: "edd"
}
]
}

ocs2 = %OrderedContentSet{
id: 2,
name: "Test Ordered Content Set 2",
profile_fields: [%ProfileField{name: "age", value: "15 - 18"}],
pages: [
%OrderedContentSetPage{
slug: "leaf-page-3",
time: 1,
unit: "day",
before_or_after: "before",
contact_field: "edd"
},
%OrderedContentSetPage{
slug: "leaf-page-4",
time: 5,
unit: "minutes",
before_or_after: "after",
contact_field: "edd"
}
]
}

ocs3 = %OrderedContentSet{
id: 3,
name: "Test Ordered Content Set3",
profile_fields: [%ProfileField{name: "relationship", value: "in a relationship"}],
pages: [
%OrderedContentSetPage{
slug: "leaf-page-5",
time: 1,
unit: "day",
before_or_after: "before",
contact_field: "edd"
},
%OrderedContentSetPage{
slug: "leaf-page-6",
time: 5,
unit: "minutes",
before_or_after: "after",
contact_field: "edd"
}
]
}

ocs4 = %OrderedContentSet{
id: 4,
name: "Test Ordered Content Set4",
profile_fields: [%ProfileField{name: "gender", value: "male"}],
pages: [
%OrderedContentSetPage{
slug: "leaf-page-7",
time: 1,
unit: "day",
before_or_after: "before",
contact_field: "edd"
},
%OrderedContentSetPage{
slug: "leaf-page-8",
time: 5,
unit: "minutes",
before_or_after: "after",
contact_field: "edd"
}
]
}

assert :ok =
FakeCMS.add_pages(wh_pid, [
topic1_index,
leaf_page,
leaf_page2,
leaf_page3,
leaf_page4,
leaf_page5,
leaf_page6,
leaf_page7,
leaf_page8
])

assert :ok =
FakeCMS.add_ordered_content_sets(wh_pid, [
ocs1,
ocs2,
ocs3,
ocs4
])

# Return the adapter.
FakeCMS.wh_adapter(wh_pid)
end

defp fake_cms(step, base_url, auth_token),
do: WH.set_adapter(step, base_url, setup_fake_cms(auth_token))

defp setup_contact_fields(context) do
context
|> FlowTester.set_contact_properties(%{
"gender" => "",
"year_of_birth" => "1988",
"relationship_status" => ""
})
end

defp setup_flow() do
auth_token = "testtoken"

flow_path("send_next_message")
|> FlowTester.from_json!()
|> fake_cms("https://content-repo-api-qa.prk-k8s.prd-p6t.org/", auth_token)
|> FlowTester.set_global_dict("config", %{"contentrepo_token" => auth_token})
|> setup_contact_fields()
end

describe "push messaging" do
test "send whatsapp template" do
fake_time = ~U[2023-02-28 00:00:00Z]
# 5.5 minutes later
future_fake_time = DateTime.add(fake_time, 330, :second)
string_fake_time = DateTime.to_iso8601(fake_time)

setup_flow()
|> FlowTester.set_fake_time(future_fake_time)
|> FlowTester.set_contact_properties(%{"push_messaging_signup" => string_fake_time})
|> FlowTester.start()
|> result_matches(%{name: "template_sent", value: "test_name"})
end

test "send regular message" do
fake_time = ~U[2023-02-28 00:00:00Z]
# 3 minutes later
future_fake_time = DateTime.add(fake_time, 180, :second)
string_fake_time = DateTime.to_iso8601(fake_time)

setup_flow()
|> FlowTester.set_fake_time(future_fake_time)
|> FlowTester.set_contact_properties(%{"push_messaging_signup" => string_fake_time})
|> FlowTester.start()
|> receive_message(%{
text: "Leaf Page 2\n\nTest leaf content page 2\n"
})
|> result_matches(%{name: "message_sent", value: "3"})
end

test "filter by relationship status" do
fake_time = ~U[2023-02-28 00:00:00Z]
# 3 minutes later
future_fake_time = DateTime.add(fake_time, 180, :second)
string_fake_time = DateTime.to_iso8601(fake_time)

setup_flow()
|> FlowTester.set_fake_time(future_fake_time)
|> FlowTester.set_contact_properties(%{
"push_messaging_signup" => string_fake_time,
"relationship_status" => "in a relationship"
})
|> FlowTester.start()
|> receive_message(%{
text: "Leaf Page 6\n\nTest leaf content page 6\n"
})
|> result_matches(%{name: "message_sent", value: "7"})
end

test "filter by age range" do
fake_time = ~U[2023-02-28 00:00:00Z]
# 3 minutes later
future_fake_time = DateTime.add(fake_time, 180, :second)
string_fake_time = DateTime.to_iso8601(fake_time)

setup_flow()
|> FlowTester.set_fake_time(future_fake_time)
|> FlowTester.set_contact_properties(%{
"year_of_birth" => "2010",
"push_messaging_signup" => string_fake_time
})
|> FlowTester.start()
|> receive_message(%{
text: "Leaf Page 2\n\nTest leaf content page 2\n"
})
|> result_matches(%{name: "message_sent", value: "3"})
end

test "filter by gender" do
fake_time = ~U[2023-02-28 00:00:00Z]
# 3 minutes later
future_fake_time = DateTime.add(fake_time, 180, :second)
string_fake_time = DateTime.to_iso8601(fake_time)

setup_flow()
|> FlowTester.set_fake_time(future_fake_time)
|> FlowTester.set_contact_properties(%{
"gender" => "male",
"push_messaging_signup" => string_fake_time
})
|> FlowTester.start()
|> receive_message(%{
text: "Leaf Page 8\n\nTest leaf content page 8\n"
})
|> result_matches(%{name: "message_sent", value: "9"})
end
end
end
150 changes: 150 additions & 0 deletions Push messaging/QA/tests/signup_test.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
defmodule SignupTest do
use FlowTester.Case
use FakeCMS

alias FlowTester.WebhookHandler, as: WH

defp flow_path(flow_name), do: Path.join([__DIR__, "..", "flows_json", flow_name <> ".json"])

def setup_fake_cms(auth_token) do
# Start the handler.
wh_pid = start_link_supervised!({FakeCMS, %FakeCMS.Config{auth_token: auth_token}})

topic1_index = %Index{slug: "topic-1", title: "Topic 1"}

leaf_page = %ContentPage{
parent: "topic-1",
slug: "leaf-page-1",
title: "Leaf Page 1",
wa_messages: [
%WAMsg{
message: "Test leaf content page",
buttons: [%Btn.Next{title: "Next"}]
},
%WAMsg{
message: "Last message"
}
]
}

leaf_page2 = %ContentPage{
parent: "topic-1",
slug: "leaf-page-2",
title: "Leaf Page 2",
wa_messages: [
%WAMsg{
message: "Test leaf content page 2",
buttons: [%Btn.Next{title: "Next"}]
},
%WAMsg{
message: "Last message"
}
]
}

ocs1 = %OrderedContentSet{
id: 1,
name: "Test Ordered Content Set",
profile_fields: [%ProfileField{name: "relationship", value: "single"}],
pages: [
%OrderedContentSetPage{
slug: "leaf-page-1",
time: 1,
unit: "day",
before_or_after: "before",
contact_field: "edd"
}
]
}

ocs2 = %OrderedContentSet{
id: 2,
name: "Test Ordered Content Set 2",
profile_fields: [%ProfileField{name: "relationship", value: "it's complicated"}],
pages: [
%OrderedContentSetPage{
slug: "leaf-page-2",
time: 5,
unit: "minutes",
before_or_after: "after",
contact_field: "edd"
}
]
}

assert :ok =
FakeCMS.add_pages(wh_pid, [
topic1_index,
leaf_page,
leaf_page2
])

assert :ok =
FakeCMS.add_ordered_content_sets(wh_pid, [
ocs1,
ocs2
])

# Return the adapter.
FakeCMS.wh_adapter(wh_pid)
end

defp fake_cms(step, base_url, auth_token),
do: WH.set_adapter(step, base_url, setup_fake_cms(auth_token))

defp setup_contact_fields(context) do
context
|> FlowTester.set_contact_properties(%{"gender" => "", "age" => "", "relationship" => ""})
end

defp setup_flow() do
auth_token = "testtoken"

flow_path("signup")
|> FlowTester.from_json!()
|> fake_cms("https://content-repo-api-qa.prk-k8s.prd-p6t.org/", auth_token)
|> FlowTester.set_global_dict("config", %{"contentrepo_token" => auth_token})
|> setup_contact_fields()
end

describe "push messaging signup" do
test "AskSignup" do
setup_flow()
|> FlowTester.start()
|> receive_message(%{
text:
"Hi!\n\nWould you like to sign up to our testing messaging set?\n\nYou will receive 5 messages, one every 5 minutes.\n",
buttons: [{"Yes, please", "Yes, please"}, {"No, thank you", "No, thank you"}]
})
end

test "AskSignup -> Exit" do
setup_flow()
|> FlowTester.start()
|> receive_message(%{})
|> FlowTester.send("No, thank you")
|> receive_message(%{
text: "Thank you! You will not be sent any messages."
})
|> result_matches(%{name: "push_messaging_signup", value: "no"})
|> flow_finished()
end

test "AskSignup -> CompleteSignup" do
fake_time = ~U[2023-02-28 00:00:00Z]
string_fake_time = DateTime.to_iso8601(fake_time)

setup_flow()
|> FlowTester.set_fake_time(fake_time)
|> FlowTester.start()
|> receive_message(%{})
|> FlowTester.send("Yes, please")
|> receive_message(%{
text: "Thank you for signing up! You will receive your first message shortly"
})
|> contact_matches(%{"push_messaging_signup" => ^string_fake_time})
|> result_matches(%{name: "push_messaging_signup", value: "1"})
|> flow_finished()
end
end
end
8 changes: 8 additions & 0 deletions Push messaging/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# Push Messaging
Push messaging uses the additional features of ContentRepo's ordered content sets, where you can set a contact field, and a relative time, for each content page in the set.

These flows will then take that ordered content set, and send those pages to the user using the schedule provided.

This works using Turn's triggers with a time relative to the contact field `push_messaging_signup`. Instead of explicitly keeping track of which message index we have / should send to the user, we calculate which message should be sent using `push_messaging_signup` as well. This means that when a message is added to or removed from CMS the recipient doesn't miss any messages, but it does mean that the journey has to be changed in lockstep by
1. Adding the trigger for the new message
1. Adding the relavent `DetermineMessage` card with correct conditions
15 changes: 15 additions & 0 deletions Push messaging/stacks_config.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
base_url: https://whatsapp.turn.io/
prod_dir: Prod
qa_dir: QA
stack_uuids:
- name: signup
qa_uuid: ec6311dc-6447-4701-9c35-7e4a26ec6c3f
prod_uuid: todo
- name: send_next_message
qa_uuid: 55b1690d-a546-4d33-912c-7f2b83665ef0
prod_uuid: todo
tokens: []
urls:
- name: contentrepo
prod_url: https://platform-mnch-contentrepo.prk-k8s.prd-p6t.org
qa_url: https://content-repo-api-qa.prk-k8s.prd-p6t.org
Original file line number Diff line number Diff line change
@@ -1,55 +1,63 @@
<!--
dictionary: "config"
version: "0.1.0"
columns: []
-->

| Key | Value |
| ----------------- | ---------------------------------------- |
| contentrepo_token | xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx |

This stack schedules the send message callback stack, at the correct time as specified on the current message of the ordered content set.

It fetches the details from the contentrepo for the user's current content set.

If there are no more messages in the content set, then it sends the user a message that they've completed the content set.

It then calculates the timestamp when the next message should be sent, according to the specified contact field and time delta specified in content repo.

<!-- { section: "c1af92c3-f489-4f4b-8182-865897f83ea1", x: 0, y: 0} -->

```stack
card GetContentSet, then: ScheduleNextMessage do
contentset =
get(
"https://content-repo-api-qa.prk-k8s.prd-p6t.org/api/v2/orderedcontent/@contact.push_messaging_content_set/",
headers: [
["Authorization", "Token @config.items.contentrepo_token"]
]
)
end
card ScheduleNextMessage
when count(contentset.body.pages) == contact.push_messaging_content_set_position do
text("Content set complete, no more messages")
end
card ScheduleNextMessage do
page = contentset.body.pages[contact.push_messaging_content_set_position]
contact_field = page.contact_field
unit =
find(
[["minutes", "m"], ["hours", "h"], ["days", "D"], ["months", "M"]],
&(&1[0] == page.unit)
)[1]
offset = if(page.before_or_after == "before", page.time * -1, page.time * 1)
timestamp = datetime_add(contact[contact_field], offset, unit)
# SBM: Schedule message callback
schedule_stack("54e7fe5d-983a-4292-a768-ca2e95466a6a", at: timestamp)
write_result("message_scheduled_at", "@timestamp")
end
# Stage Based Messaging: Schedule Next Push Message

This stack schedules the send message callback stack, at the correct time as specified on the current message of the ordered content set.

It fetches the details from the contentrepo for the user's current content set.

If there are no more messages in the content set, then it sends the user a message that they've completed the content set.

It then calculates the timestamp when the next message should be sent, according to the specified contact field and time delta specified in content repo.

## Configuration

This Journey requires the `config.contentrepo_token` global variable to be set.

## Contact fields

This Journey doesn't use or set any contact fields

## Flow results

* message_scheduled_at, when the message is scheduled for

## Connections to other stacks

* Schedules the stack to send the next push message

<!-- { section: "c1af92c3-f489-4f4b-8182-865897f83ea1", x: 0, y: 0} -->

```stack
card GetContentSet, then: ScheduleNextMessage do
contentset =
get(
"https://content-repo-api-qa.prk-k8s.prd-p6t.org/api/v2/orderedcontent/@contact.push_messaging_content_set/",
headers: [
["Authorization", "Token @global.config.contentrepo_token"]
]
)
end
card ScheduleNextMessage
when count(contentset.body.pages) == contact.push_messaging_content_set_position do
text("Content set complete, no more messages")
end
card ScheduleNextMessage do
page = contentset.body.pages[contact.push_messaging_content_set_position]
contact_field = page.contact_field
unit =
find(
[["minutes", "m"], ["hours", "h"], ["days", "D"], ["months", "M"]],
&(&1[0] == page.unit)
)[1]
offset = if(page.before_or_after == "before", page.time * -1, page.time * 1)
timestamp = datetime_add(contact[contact_field], offset, unit)
# SBM: Schedule message callback
schedule_stack("8eb4490c-dc45-4c1f-bf10-1a95158ef45f", at: timestamp)
write_result("message_scheduled_at", "@timestamp")
end
```
Original file line number Diff line number Diff line change
@@ -1,72 +1,79 @@
<!--
dictionary: "config"
version: "0.1.0"
columns: []
-->

| Key | Value |
| ----------------- | ---------------------------------------- |
| contentrepo_token | xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx |

This stack fetches the current message for the current message set, as defined in the contact fields.

It handles the following message types:

* Template messages. Assumes template message with two variables. Sets the first to the contact's whatsapp profile name, and the second to the literal "Second"
* Text messages. Sends the title, followed by the body, of the whatsapp message, in a single message to the user.

It then increments the content set position on the contact, and runs the stack that handles scheduling the next message in the message set.

<!-- { section: "c1af92c3-f489-4f4b-8182-865897f83ea1", x: 0, y: 0} -->

```stack
card GetMessage, then: SendMessage do
contentset =
get(
"https://content-repo-api-qa.prk-k8s.prd-p6t.org/api/v2/orderedcontent/@contact.push_messaging_content_set/",
headers: [
["Authorization", "Token @config.items.contentrepo_token"]
]
)
contentset_item = contentset.body.pages[contact.push_messaging_content_set_position]
page =
get(
"https://content-repo-api-qa.prk-k8s.prd-p6t.org/api/v2/pages/@contentset_item.id/",
headers: [
["Authorization", "Token @config.items.contentrepo_token"]
],
query: [["whatsapp", "true"]]
)
end
card SendMessage when page.body.body.is_whatsapp_template, then: ScheduleNextMessage do
write_result("template_sent", "@page.body.body.whatsapp_template_name")
send_message_template("@page.body.body.whatsapp_template_name", "en_US", [
"@contact.whatsapp_profile_name",
"Second"
])
end
card SendMessage, then: ScheduleNextMessage do
write_result("message_sent", "@page.body.id")
text("""
@page.body.title
@page.body.body.text.value.message
""")
end
card ScheduleNextMessage do
update_contact(
push_messaging_content_set_position: "@(contact.push_messaging_content_set_position + 1)"
)
# SBM: Schedule next push message
run_stack("f7a966e0-2945-455e-a3d2-519b750e20aa")
end
This stack fetches the current message for the current message set, as defined in the contact fields.

It handles the following message types:

* Template messages. Assumes template message with two variables. Sets the first to the contact's whatsapp profile name, and the second to the literal "Second"
* Text messages. Sends the title, followed by the body, of the whatsapp message, in a single message to the user.

It then increments the content set position on the contact, and runs the stack that handles scheduling the next message in the message set.

## Configuration

This Journey requires the `config.contentrepo_token` global variable to be set.

## Contact fields

* push_messaging_content_set_position: Start at 0, to always start at the beginning of the message set
* whatsapp_profile_name, used to personalise the template sent to the user

## Flow results

* message_sent, The message sent to the user

## Connections to other stacks

* Runs the stack to schedule the next push message at the end

<!-- { section: "c1af92c3-f489-4f4b-8182-865897f83ea1", x: 0, y: 0} -->

```stack
card GetMessage, then: SendMessage do
contentset =
get(
"https://content-repo-api-qa.prk-k8s.prd-p6t.org/api/v2/orderedcontent/@contact.push_messaging_content_set/",
headers: [
["Authorization", "Token @global.config.contentrepo_token"]
]
)
contentset_item = contentset.body.pages[contact.push_messaging_content_set_position]
page =
get(
"https://content-repo-api-qa.prk-k8s.prd-p6t.org/api/v2/pages/@contentset_item.id/",
headers: [
["Authorization", "Token @global.config.contentrepo_token"]
],
query: [["whatsapp", "true"]]
)
end
card SendMessage when page.body.body.is_whatsapp_template, then: ScheduleNextMessage do
write_result("template_sent", "@page.body.body.whatsapp_template_name")
send_message_template("@page.body.body.whatsapp_template_name", "en_US", [
"@contact.whatsapp_profile_name",
"Second"
])
end
card SendMessage, then: ScheduleNextMessage do
write_result("message_sent", "@page.body.id")
text("""
@page.body.title
@page.body.body.text.value.message
""")
end
card ScheduleNextMessage do
update_contact(
push_messaging_content_set_position: "@(contact.push_messaging_content_set_position + 1)"
)
# SBM: Schedule next push message
run_stack("f291b782-72d3-49eb-8434-e47e388c2ea1")
end
```
Original file line number Diff line number Diff line change
@@ -1,63 +1,73 @@
<!--
dictionary: "config"
version: "0.1.0"
columns: []
-->

| Key | Value |
| ----------------- | ---------------------------------------- |
| contentrepo_token | xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx |

This flow asks the user whether they want to sign up for the testing stage based messaging.

If they decline, then we exit

If they accept, then we search the ordered content sets on contentrepo for one called "demo", and store the following values on the contact:

* push_messaging_signup: The timestamp when the user accepts the sign up
* push_messaging_content_set: The ID of the ordered content set named "demo"
* push_messaging_content_set_position: Start at 0, to always start at the beginning of the message set

<!-- { section: "c1af92c3-f489-4f4b-8182-865897f83ea1", x: 0, y: 0} -->

```stack
card AskSignup do
buttons(FetchContentSet: "Yes, please", Exit: "No, thank you") do
text("""
Hi!
Would you like to sign up to our testing messaging set?
You will receive 5 messages, one every 5 minutes.
""")
end
end
card FetchContentSet, then: CompleteSignup do
contentsets =
get(
"https://content-repo-api-qa.prk-k8s.prd-p6t.org/api/v2/orderedcontent/",
headers: [
["Authorization", "Token @config.items.contentrepo_token"]
]
)
contentset = find(contentsets.body.results, &(&1.name == "demo"))
end
card CompleteSignup do
write_result("sbm_signup", "@contentset.id")
update_contact(push_messaging_signup: "@now()")
update_contact(push_messaging_content_set: "@contentset.id")
update_contact(push_messaging_content_set_position: 0)
text("Thank you for signing up! You will receive your first message shortly")
# SBM: Schedule next push message
run_stack("f7a966e0-2945-455e-a3d2-519b750e20aa")
end
card Exit do
write_result("sbm_signup", "no")
text("Thank you! You will not be sent any messages")
end
# Stage Based Messaging: Signup

This flow asks the user whether they want to sign up for the testing stage based messaging.

If they decline, then we exit

If they accept, then we search the ordered content sets on contentrepo for one called "demo", and store the following values on the contact:

* push_messaging_signup: The timestamp when the user accepts the sign up
* push_messaging_content_set: The ID of the ordered content set named "demo"
* push_messaging_content_set_position: Start at 0, to always start at the beginning of the message set

## Configuration

This Journey requires the `config.contentrepo_token` global variable to be set.

## Contact fields

* push_messaging_signup: The timestamp when the user accepts the sign up
* push_messaging_content_set: The ID of the ordered content set named "demo"
* push_messaging_content_set_position: Start at 0, to always start at the beginning of the message set

## Flow results

* sbm_signup, whether or not the user signed up for push messages

## Connections to other stacks

* Runs the stack to schedule the next push message

<!-- { section: "c1af92c3-f489-4f4b-8182-865897f83ea1", x: 0, y: 0} -->

```stack
card AskSignup do
buttons(FetchContentSet: "Yes, please", Exit: "No, thank you") do
text("""
Hi!
Would you like to sign up to our testing messaging set?
You will receive 5 messages, one every 5 minutes.
""")
end
end
card FetchContentSet, then: CompleteSignup do
contentsets =
get(
"https://content-repo-api-qa.prk-k8s.prd-p6t.org/api/v2/orderedcontent/",
headers: [
["Authorization", "Token @global.config.contentrepo_token"]
]
)
contentset = find(contentsets.body.results, &(&1.name == "demo"))
end
card CompleteSignup do
write_result("sbm_signup", "@contentset.id")
update_contact(push_messaging_signup: "@now()")
update_contact(push_messaging_content_set: "@contentset.id")
update_contact(push_messaging_content_set_position: 0)
text("Thank you for signing up! You will receive your first message shortly")
# SBM: Schedule next push message
run_stack("f291b782-72d3-49eb-8434-e47e388c2ea1")
end
card Exit do
write_result("sbm_signup", "no")
text("Thank you! You will not be sent any messages")
end
```
14 changes: 14 additions & 0 deletions Stage based messaging/stacks_config.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
urls:
- name: contentrepo
qa_url: https://content-repo-api-qa.prk-k8s.prd-p6t.org
prod_url: https://platform-mnch-contentrepo.prk-k8s.prd-p6t.org
stack_uuids:
- name: signup
qa_uuid: ce9d3d33-8760-47fe-a6c2-298991cb5764
prod_uuid: todo
- name: schedule_next_push_message
qa_uuid: f291b782-72d3-49eb-8434-e47e388c2ea1
prod_uuid: todo
- name: send_next_message_callback
qa_uuid: 8eb4490c-dc45-4c1f-bf10-1a95158ef45f
prod_uuid: todo