diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index 06ac61e..0000000 --- a/requirements.txt +++ /dev/null @@ -1,5 +0,0 @@ -fastapi==0.115.3 -python-dotenv==1.0.1 -Requests==2.32.3 -uvicorn==0.32.0 -httpx==0.23.0 diff --git a/routes/routes121.py b/routes/routes121.py index 8c6f274..238cffe 100644 --- a/routes/routes121.py +++ b/routes/routes121.py @@ -2,6 +2,9 @@ import requests import re import os +import csv +import pandas as pd +from datetime import datetime, timedelta from fastapi.responses import JSONResponse from utils.utilsKobo import clean_kobo_data, get_attachment_dict, required_headers_kobo from utils.utils121 import login121, required_headers_121, clean_text @@ -119,7 +122,7 @@ async def kobo_to_121(request: Request, dependencies=Depends(required_headers_12 ######################################################################################################################## @router.post("/kobo-update-121") -async def kobo_update_121(request: Request, dependencies=Depends(required_headers_121)): +async def kobo_update_121(request: Request, dependencies=Depends(required_headers_121), test_mode: bool = False): """Update a 121 record from a Kobo submission""" kobo_data = await request.json() @@ -156,8 +159,6 @@ async def kobo_update_121(request: Request, dependencies=Depends(required_header extra_logs["121_program_id"] = programid referenceId = kobo_data['referenceid'] - - access_token = login121(request.headers["url121"], request.headers["username121"], request.headers["password121"]) # Create API payload body intvalues = ['maxPayments', 'paymentAmountMultiplier', 'inclusionScore'] @@ -179,6 +180,11 @@ async def kobo_update_121(request: Request, dependencies=Depends(required_header else: payload["data"][target_field] = attachments[kobo_value_url]['url'] + if test_mode: + return JSONResponse(status_code=200, content={"payload": payload}) + + access_token = login121(request.headers["url121"], request.headers["username121"], request.headers["password121"]) + # POST to target API if target_field != 'referenceId': response = requests.patch( @@ -226,7 +232,7 @@ async def kobo_update_121(request: Request, dependencies=Depends(required_header ########### @router.get("/121-program") async def create_121_program_from_kobo( - request: Request, dependencies=Depends(required_headers_kobo) + request: Request, dependencies=Depends(required_headers_kobo), test_mode: bool = False ): """Utility endpoint to automatically create a 121 Program in 121 from a koboform, including REST Service \n Does only support the IFRC server kobo.ifrc.org \n @@ -402,6 +408,9 @@ async def create_121_program_from_kobo( } data["programQuestions"].append(question) + if test_mode: + return JSONResponse(status_code=200, content=data) + # Create kobo-connect rest service restServicePayload = { "name": "Kobo Connect", diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..5456203 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,9 @@ +# # tests/conftest.py + +# import sys +# import os + +# # Ensure the root directory is in the PYTHONPATH +# sys.path.append(os.path.dirname(os.path.abspath(__file__)) + '/../') + +# print(sys.path) \ No newline at end of file diff --git a/tests/program121.json b/tests/program121.json new file mode 100644 index 0000000..32bbf1d --- /dev/null +++ b/tests/program121.json @@ -0,0 +1,600 @@ +{ + "published":true, + "validation":true, + "phase":"registrationValidation", + "location":"Netherlands", + "ngo":"RCRC", + "titlePortal":{ + "en":"Template Create Program" + }, + "titlePaApp":{ + "en":"Template Create Program" + }, + "description":{ + "en":"" + }, + "startDate":"2024-07-03T00:00:00", + "endDate":"2024-07-31T00:00:00", + "currency":"EUR", + "distributionFrequency":"week", + "distributionDuration":4, + "fixedTransferValue":100, + "paymentAmountMultiplierFormula":"", + "financialServiceProviders":[ + { + "fsp":"Commercial-bank-ethiopia" + } + ], + "targetNrRegistrations":10, + "tryWhatsAppFirst":false, + "phoneNumberPlaceholder":"32000000000", + "programCustomAttributes":[ + + ], + "programQuestions":[ + { + "name":"province", + "label":{ + "en":"What is the province?" + }, + "answerType":"dropdown", + "questionType":"standard", + "options":[ + { + "option":"South-Holland", + "label":{ + "en":"South-Holland" + } + }, + { + "option":"North-Holland", + "label":{ + "en":"North-Holland" + } + } + ], + "scoring":{ + + }, + "persistence":true, + "pattern":"", + "phases":[ + + ], + "editableInPortal":true, + "export":[ + "all-people-affected", + "included" + ], + "shortLabel":{ + "en":"province" + }, + "duplicateCheck":false, + "placeholder":"" + }, + { + "name":"city", + "label":{ + "en":"What is the city?" + }, + "answerType":"dropdown", + "questionType":"standard", + "options":[ + { + "option":"Amsterdam", + "label":{ + "en":"Amsterdam" + } + }, + { + "option":"Haarlem", + "label":{ + "en":"Haarlem" + } + }, + { + "option":"Den Haag", + "label":{ + "en":"Den Haag" + } + }, + { + "option":"Rotterdam", + "label":{ + "en":"Rotterdam" + } + } + ], + "scoring":{ + + }, + "persistence":true, + "pattern":"", + "phases":[ + + ], + "editableInPortal":true, + "export":[ + "all-people-affected", + "included" + ], + "shortLabel":{ + "en":"city" + }, + "duplicateCheck":false, + "placeholder":"" + }, + { + "name":"scope", + "label":{ + "en":"scope" + }, + "answerType":"text", + "questionType":"standard", + "options":[ + + ], + "scoring":{ + + }, + "persistence":true, + "pattern":"", + "phases":[ + + ], + "editableInPortal":true, + "export":[ + "all-people-affected", + "included" + ], + "shortLabel":{ + "en":"scope" + }, + "duplicateCheck":false, + "placeholder":"" + }, + { + "name":"fullName", + "label":{ + "en":"What is your name?" + }, + "answerType":"text", + "questionType":"standard", + "options":[ + + ], + "scoring":{ + + }, + "persistence":true, + "pattern":"", + "phases":[ + + ], + "editableInPortal":true, + "export":[ + "all-people-affected", + "included" + ], + "shortLabel":{ + "en":"fullName" + }, + "duplicateCheck":true, + "placeholder":"" + }, + { + "name":"age", + "label":{ + "en":"What is your age?" + }, + "answerType":"numeric", + "questionType":"standard", + "options":[ + + ], + "scoring":{ + + }, + "persistence":true, + "pattern":"", + "phases":[ + + ], + "editableInPortal":true, + "export":[ + "all-people-affected", + "included" + ], + "shortLabel":{ + "en":"age" + }, + "duplicateCheck":false, + "placeholder":"" + }, + { + "name":"sex", + "label":{ + "en":"What is your sex?" + }, + "answerType":"dropdown", + "questionType":"standard", + "options":[ + { + "option":"Female", + "label":{ + "en":"Female" + } + }, + { + "option":"Male", + "label":{ + "en":"Male" + } + }, + { + "option":"Other", + "label":{ + "en":"Other" + } + } + ], + "scoring":{ + + }, + "persistence":true, + "pattern":"", + "phases":[ + + ], + "editableInPortal":true, + "export":[ + "all-people-affected", + "included" + ], + "shortLabel":{ + "en":"sex" + }, + "duplicateCheck":false, + "placeholder":"" + }, + { + "name":"idNumber", + "label":{ + "en":"What is your ID number?" + }, + "answerType":"numeric", + "questionType":"standard", + "options":[ + + ], + "scoring":{ + + }, + "persistence":true, + "pattern":"", + "phases":[ + + ], + "editableInPortal":true, + "export":[ + "all-people-affected", + "included" + ], + "shortLabel":{ + "en":"idNumber" + }, + "duplicateCheck":true, + "placeholder":"" + }, + { + "name":"phoneNumber", + "label":{ + "en":"Phone Number" + }, + "answerType":"tel", + "questionType":"standard", + "options":[ + + ], + "scoring":{ + + }, + "persistence":true, + "pattern":"", + "phases":[ + + ], + "editableInPortal":true, + "export":[ + "all-people-affected", + "included" + ], + "shortLabel":{ + "en":"phoneNumber" + }, + "duplicateCheck":true, + "placeholder":"" + }, + { + "name":"hhSize", + "label":{ + "en":"What is the amount of members in your household?" + }, + "answerType":"numeric", + "questionType":"standard", + "options":[ + + ], + "scoring":{ + + }, + "persistence":true, + "pattern":"", + "phases":[ + + ], + "editableInPortal":true, + "export":[ + "all-people-affected", + "included" + ], + "shortLabel":{ + "en":"hhSize" + }, + "duplicateCheck":false, + "placeholder":"" + }, + { + "name":"selectionSingleHeaded", + "label":{ + "en":"Is this a single parent headed household?" + }, + "answerType":"dropdown", + "questionType":"standard", + "options":[ + { + "option":"Yes", + "label":{ + "en":"Yes" + } + }, + { + "option":"No", + "label":{ + "en":"No" + } + } + ], + "scoring":{ + + }, + "persistence":true, + "pattern":"", + "phases":[ + + ], + "editableInPortal":true, + "export":[ + "all-people-affected", + "included" + ], + "shortLabel":{ + "en":"selectionSingleHeaded" + }, + "duplicateCheck":false, + "placeholder":"" + }, + { + "name":"selectionElderlyHeaded", + "label":{ + "en":"Is this an elderly headed household?" + }, + "answerType":"dropdown", + "questionType":"standard", + "options":[ + { + "option":"Yes", + "label":{ + "en":"Yes" + } + }, + { + "option":"No", + "label":{ + "en":"No" + } + } + ], + "scoring":{ + + }, + "persistence":true, + "pattern":"", + "phases":[ + + ], + "editableInPortal":true, + "export":[ + "all-people-affected", + "included" + ], + "shortLabel":{ + "en":"selectionElderlyHeaded" + }, + "duplicateCheck":false, + "placeholder":"" + }, + { + "name":"selectionIllness", + "label":{ + "en":"Are there people with serious and/or chronic illness in the household?" + }, + "answerType":"dropdown", + "questionType":"standard", + "options":[ + { + "option":"Yes", + "label":{ + "en":"Yes" + } + }, + { + "option":"No", + "label":{ + "en":"No" + } + } + ], + "scoring":{ + + }, + "persistence":true, + "pattern":"", + "phases":[ + + ], + "editableInPortal":true, + "export":[ + "all-people-affected", + "included" + ], + "shortLabel":{ + "en":"selectionIllness" + }, + "duplicateCheck":false, + "placeholder":"" + }, + { + "name":"selectionDisability", + "label":{ + "en":"Are there people with disabilities in the household?" + }, + "answerType":"dropdown", + "questionType":"standard", + "options":[ + { + "option":"Yes", + "label":{ + "en":"Yes" + } + }, + { + "option":"No", + "label":{ + "en":"No" + } + } + ], + "scoring":{ + + }, + "persistence":true, + "pattern":"", + "phases":[ + + ], + "editableInPortal":true, + "export":[ + "all-people-affected", + "included" + ], + "shortLabel":{ + "en":"selectionDisability" + }, + "duplicateCheck":false, + "placeholder":"" + }, + { + "name":"selectionDisplaced", + "label":{ + "en":"Is the household displaced?" + }, + "answerType":"dropdown", + "questionType":"standard", + "options":[ + { + "option":"Yes", + "label":{ + "en":"Yes" + } + }, + { + "option":"No", + "label":{ + "en":"No" + } + } + ], + "scoring":{ + + }, + "persistence":true, + "pattern":"", + "phases":[ + + ], + "editableInPortal":true, + "export":[ + "all-people-affected", + "included" + ], + "shortLabel":{ + "en":"selectionDisplaced" + }, + "duplicateCheck":false, + "placeholder":"" + }, + { + "name":"selectionUnemployment", + "label":{ + "en":"Is the head of household unemployed?" + }, + "answerType":"dropdown", + "questionType":"standard", + "options":[ + { + "option":"Yes", + "label":{ + "en":"Yes" + } + }, + { + "option":"No", + "label":{ + "en":"No" + } + } + ], + "scoring":{ + + }, + "persistence":true, + "pattern":"", + "phases":[ + + ], + "editableInPortal":true, + "export":[ + "all-people-affected", + "included" + ], + "shortLabel":{ + "en":"selectionUnemployment" + }, + "duplicateCheck":false, + "placeholder":"" + } + ], + "aboutProgram":{ + "en":"Any text here" + }, + "fullnameNamingConvention":[ + "fullName" + ], + "languages":[ + "en" + ], + "enableMaxPayments":true, + "allowEmptyPhoneNumber":false, + "enableScope":false + } \ No newline at end of file diff --git a/tests/test_121program.py b/tests/test_121program.py new file mode 100644 index 0000000..9a4708c --- /dev/null +++ b/tests/test_121program.py @@ -0,0 +1,27 @@ +import sys +import os +import json +from fastapi.testclient import TestClient +from dotenv import load_dotenv + +sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))) + +from main import app + +client = TestClient(app) + +# load environment variables +load_dotenv() +kobotoken = os.environ["TEST_KOBO_TOKEN"] +koboassetid = os.environ["TEST_KOBO_ASSETID"] + +with open(os.path.join(os.path.dirname(__file__), 'program121.json'), 'r') as file: + program121 = json.load(file) + +def test_121_program(): + headers = {"kobotoken": kobotoken, "koboasset": koboassetid} + response = client.get("/121-program?test_mode=true", headers=headers) + assert response.status_code == 200 + + response_data = response.json() + assert response_data == program121 \ No newline at end of file diff --git a/tests/test_koboupdate121.py b/tests/test_koboupdate121.py new file mode 100644 index 0000000..1c61d13 --- /dev/null +++ b/tests/test_koboupdate121.py @@ -0,0 +1,33 @@ +import sys +import os +import json +from fastapi.testclient import TestClient + +sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))) + +from main import app + +client = TestClient(app) + +with open(os.path.join(os.path.dirname(__file__), 'kobo_data.json'), 'r') as file: + kobo_data = json.load(file) + +with open(os.path.join(os.path.dirname(__file__), 'kobo_headers.json'), 'r') as file: + kobo_headers = json.load(file) + +kobo_data['referenceid'] = "test121" + +def test_kobo_to_121_payload(): + + response = client.post( + "/kobo-update-121?test_mode=true", + headers=kobo_headers, + json=kobo_data + ) + + response_data = response.json() + print(response_data) + + assert response.status_code == 200 + assert "payload" in response_data + assert response_data == {'payload': {'data': {'bankaccountnumber': '12345678'}, 'reason': 'Validated during field validation'}}