From 98142ffbc037ab0165ae6f4fcf852609e8e420ce Mon Sep 17 00:00:00 2001 From: Erik Harding Date: Wed, 17 Jan 2024 13:07:36 +0200 Subject: [PATCH] Add facebook invite active flags --- vaccine/testing.py | 1 + yal/surveys/facebook_invite.py | 53 +++++++++- yal/tests/states_dictionary.md | 19 ++-- yal/tests/surveys/test_facebook_invite.py | 114 +++++++++++++++++++++- 4 files changed, 175 insertions(+), 12 deletions(-) diff --git a/vaccine/testing.py b/vaccine/testing.py index 67e68655..65bbbff1 100644 --- a/vaccine/testing.py +++ b/vaccine/testing.py @@ -191,6 +191,7 @@ class TState: errormax: int = 0 banner: bool = False contact_fields: dict = field(default_factory=dict) + globals: dict = field(default_factory=dict) def unused_port(): diff --git a/yal/surveys/facebook_invite.py b/yal/surveys/facebook_invite.py index e0240bb6..cc64084d 100644 --- a/yal/surveys/facebook_invite.py +++ b/yal/surveys/facebook_invite.py @@ -1,18 +1,26 @@ from vaccine.base_application import BaseApplication from vaccine.states import Choice, EndState, WhatsAppButtonState -from yal import config +from yal import config, rapidpro from yal.utils import get_generic_error class Application(BaseApplication): - START_STATE = "state_facebook_member" + START_STATE = "state_check_study_active" NOT_INTERESTED_STATE = "state_facebook_invite_decline" + async def state_check_study_active(self): + status = await rapidpro.get_global_value("facebook_survey_status") + + if status == "inactive": + return await self.go_to_state("state_facebook_study_not_active") + + return await self.go_to_state("state_facebook_member") + async def state_facebook_member(self): async def _next(choice: Choice): if choice.value == "no": return "state_not_a_member" - return "state_was_a_member" + return "state_check_survey_active" return WhatsAppButtonState( self, @@ -28,6 +36,14 @@ async def _next(choice: Choice): error=self._(get_generic_error()), ) + async def state_check_survey_active(self): + status = await rapidpro.get_global_value("facebook_survey_status") + + if status == "study_b_only": + return await self.go_to_state("state_facebook_study_not_active") + + return await self.go_to_state("state_was_a_member") + async def state_was_a_member(self): return EndState( self, @@ -115,3 +131,34 @@ async def state_facebook_invite_decline(self): ), next=self.START_STATE, ) + + async def state_facebook_study_not_active(self): + choices = [ + Choice("menu", "Go to the menu"), + Choice("aaq", "Ask a question"), + ] + + question = self._( + "\n".join( + [ + "Eish! It looks like you just missed the cut off for our survey. " + "No worries, we get it, life happens!", + "", + "Stay tuned for more survey opportunities. We appreciate your " + "enthusiasm and hope you can catch the next one.", + "", + "Go ahead and browse the menu or ask us a question.", + ] + ) + ) + + return WhatsAppButtonState( + self, + question=question, + choices=choices, + error=self._(get_generic_error()), + next={ + "menu": "state_pre_mainmenu", + "aaq": "state_aaq_start", + }, + ) diff --git a/yal/tests/states_dictionary.md b/yal/tests/states_dictionary.md index 9a7b6b42..24ee96e0 100644 --- a/yal/tests/states_dictionary.md +++ b/yal/tests/states_dictionary.md @@ -832,11 +832,14 @@ ## Facebook Survey Invite -| state_name | accepts_user_input | data_type | added_to_flow_results_app | description | -| ----------------------------- | ------------------ | --------- | ------------------------- | ----------------------------------------------------------------| -| state_facebook_member | TRUE | Text | TRUE | Asks user if they've been a member of the bwise facebook group. | -| state_was_a_member | FALSE | | | Shows user the relavant survey link | -| state_not_a_member | TRUE | Text | TRUE | Asks user if they've seen a bwise post in their FB feed | -| state_fb_feed_seen | FALSE | | | Shows user the relavant survey link | -| state_fb_feed_not_seen | FALSE | | | Tells user they're not eligible | -| state_facebook_invite_decline | FALSE | | | Confirmation message | +| state_name | accepts_user_input | data_type | added_to_flow_results_app | description | +| ------------------------------- | ------------------ | --------- | ------------------------- | ----------------------------------------------------------------| +| state_facebook_member | TRUE | Text | TRUE | Asks user if they've been a member of the bwise facebook group. | +| state_was_a_member | FALSE | | | Shows user the relavant survey link | +| state_not_a_member | TRUE | Text | TRUE | Asks user if they've seen a bwise post in their FB feed | +| state_fb_feed_seen | FALSE | | | Shows user the relavant survey link | +| state_fb_feed_not_seen | FALSE | | | Tells user they're not eligible | +| state_facebook_invite_decline | FALSE | | | Confirmation message | +| state_check_study_active | FALSE | | | Checks if the study is still active | +| state_facebook_study_not_active | TRUE | Text | TRUE | Tells user study is not active, give main and aaq buttons | +| state_check_survey_active | FALSE | | | Checks if the specific survey is still active | diff --git a/yal/tests/surveys/test_facebook_invite.py b/yal/tests/surveys/test_facebook_invite.py index be8eace1..e5ae6879 100644 --- a/yal/tests/surveys/test_facebook_invite.py +++ b/yal/tests/surveys/test_facebook_invite.py @@ -1,4 +1,5 @@ import json +from unittest import mock import pytest from sanic import Sanic, response @@ -40,6 +41,27 @@ def get_contact(request): status=200, ) + @app.route("/api/v2/globals.json", methods=["GET"]) + def get_global_value(request): + tstate.requests.append(request) + assert request.args.get("key") == "facebook_survey_status" + + return response.json( + { + "next": None, + "previous": None, + "results": [ + { + "key": "Facebook Survey Status", + "name": "facebook_survey_status", + "value": tstate.globals["facebook_survey_status"], + "modified_on": "2023-05-30T07:34:06.216776Z", + } + ], + }, + status=200, + ) + async with run_sanic(app) as server: url = config.RAPIDPRO_URL config.RAPIDPRO_URL = f"http://{server.host}:{server.port}" @@ -49,11 +71,36 @@ def get_contact(request): config.RAPIDPRO_URL = url +@pytest.fixture +async def contentrepo_api_mock(): + Sanic.test_mode = True + app = Sanic("contentrepo_api_mock") + tstate = TState() + + @app.route("/api/v2/pages", methods=["GET"]) + def get_main_menu(request): + tstate.requests.append(request) + return response.json( + { + "count": 0, + "results": [], + } + ) + + async with run_sanic(app) as server: + url = config.CONTENTREPO_API_URL + config.CONTENTREPO_API_URL = f"http://{server.host}:{server.port}" + server.tstate = tstate + yield server + config.CONTENTREPO_API_URL = url + + @pytest.mark.asyncio async def test_facebook_invite_yes(tester: AppTester, rapidpro_mock): rapidpro_mock.tstate.contact_fields["onboarding_completed"] = True rapidpro_mock.tstate.contact_fields["terms_accepted"] = True rapidpro_mock.tstate.contact_fields["facebook_survey_invite_status"] = "sent" + rapidpro_mock.tstate.globals["facebook_survey_status"] = "active" await tester.user_input("Yes, take part") tester.assert_state("state_facebook_member") @@ -72,6 +119,39 @@ async def test_facebook_invite_yes(tester: AppTester, rapidpro_mock): } +@pytest.mark.asyncio +async def test_facebook_invite_yes_study_inactive(tester: AppTester, rapidpro_mock): + rapidpro_mock.tstate.contact_fields["onboarding_completed"] = True + rapidpro_mock.tstate.contact_fields["terms_accepted"] = True + rapidpro_mock.tstate.contact_fields["facebook_survey_invite_status"] = "sent" + rapidpro_mock.tstate.globals["facebook_survey_status"] = "inactive" + + await tester.user_input("Yes, take part") + tester.assert_state("state_facebook_study_not_active") + + tester.assert_message( + "\n".join( + [ + "Eish! It looks like you just missed the cut off for our survey. " + "No worries, we get it, life happens!", + "", + "Stay tuned for more survey opportunities. We appreciate your " + "enthusiasm and hope you can catch the next one.", + "", + "Go ahead and browse the menu or ask us a question.", + ] + ) + ) + + request = rapidpro_mock.tstate.requests[1] + + assert json.loads(request.body.decode("utf-8")) == { + "fields": { + "facebook_survey_invite_status": "responded_yes", + } + } + + @pytest.mark.asyncio async def test_facebook_invite_no(tester: AppTester, rapidpro_mock): rapidpro_mock.tstate.contact_fields["onboarding_completed"] = True @@ -124,8 +204,9 @@ async def test_facebook_invite_not_invited(tester: AppTester, rapidpro_mock): @pytest.mark.asyncio -async def test_state_facebook_member_yes(tester: AppTester): +async def test_state_facebook_member_yes_active(tester: AppTester, rapidpro_mock): tester.setup_state("state_facebook_member") + rapidpro_mock.tstate.globals["facebook_survey_status"] = "active" await tester.user_input("Yes") @@ -145,6 +226,15 @@ async def test_state_facebook_member_yes(tester: AppTester): ) ) +@pytest.mark.asyncio +async def test_state_facebook_member_yes_survey_b_only(tester: AppTester, rapidpro_mock): + tester.setup_state("state_facebook_member") + rapidpro_mock.tstate.globals["facebook_survey_status"] = "study_b_only" + + await tester.user_input("Yes") + + tester.assert_state("state_facebook_study_not_active") + @pytest.mark.asyncio async def test_state_facebook_member_no(tester: AppTester): @@ -201,3 +291,25 @@ async def test_state_not_a_member_no(tester: AppTester): ] ) ) + + +@pytest.mark.asyncio +async def test_state_facebook_study_not_active_menu( + tester: AppTester, contentrepo_api_mock +): + tester.setup_state("state_facebook_study_not_active") + + await tester.user_input("Go to the menu") + + tester.assert_state("state_mainmenu") + + +@pytest.mark.asyncio +@mock.patch("yal.askaquestion.config") +async def test_state_facebook_study_not_active_aaq(mock_config, tester: AppTester): + mock_config.AAQ_URL = "http://aaq-test.com" + tester.setup_state("state_facebook_study_not_active") + + await tester.user_input("Ask a question") + + tester.assert_state("state_aaq_start")