From 27dc876d073bef3d8a0c285f33a590107f572414 Mon Sep 17 00:00:00 2001 From: Vipul Siddharth Date: Tue, 9 Jul 2024 09:48:00 +0200 Subject: [PATCH 1/3] apply license correctly (#4005) Signed-off-by: Vipul Siddharth Co-authored-by: Domenico --- LICENSE | 21 ++++----------------- docs/README.md | 29 +++++++++++++++++++++++------ 2 files changed, 27 insertions(+), 23 deletions(-) diff --git a/LICENSE b/LICENSE index 94621be296..be3f7b28e5 100644 --- a/LICENSE +++ b/LICENSE @@ -1,20 +1,7 @@ -Copyright (c) 2014 - 2024 UNICEF. All rights reserved. - -This program is free software: you can redistribute it and/or modify -it under the terms of the GNU Affero General Public License as -published by the Free Software Foundation, either version 3 of the -License, or (at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU Affero General Public License below for more details. - ------------------------------------------------------------------------- - GNU AFFERO GENERAL PUBLIC LICENSE + GNU AFFERO GENERAL PUBLIC LICENSE Version 3, 19 November 2007 - Copyright (C) 2007 Free Software Foundation, Inc. + Copyright (C) 2007 Free Software Foundation, Inc. Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. @@ -656,7 +643,7 @@ the "copyright" line and a pointer to where the full notice is found. GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License - along with this program. If not, see . + along with this program. If not, see . Also add information on how to contact you by electronic and paper mail. @@ -671,4 +658,4 @@ specific requirements. You should also get your employer (if you work as a programmer) or school, if any, to sign a "copyright disclaimer" for the program, if necessary. For more information on this, and how to apply and follow the GNU AGPL, see -. +. diff --git a/docs/README.md b/docs/README.md index fbbdc99943..4c99bdfd68 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,4 +1,4 @@ -# UNICEF Humanitarian cash Operations and Programme Ecosystem (HOPE) +# UNICEF Humanitarian cash Operations and Programme Ecosystem (HOPE) [previously known as Humanitarian Cash Transfer (HCT) Management Information System (MIS)] #### [Development environment setup instructions](technical-specification/development-setup/) @@ -9,27 +9,27 @@ HOPE is UNICEF's humanitarian cash transfer corporate platform that is used sinc **Summary:** The HOPE system allows organization disbursing cash transfers to perform the following function with programme quality assurance, data protection standards and a risk informed cash transfer delivery for Humanitarian Cash Transfers as well as incentive payments for frontline workers. HOPE offers the following high level functiinalities: 1. Collect beneficiary data. -2. Associate data with cash transfers programmes +2. Associate data with cash transfers programmes 3. Create target populations. 4. Manage payment lists. 5. Send payment lists to Financial Service Providers (FSPs). 6. Reconcile payments. 7. Triangulate payment verification information directly from beneficiaries. -8. Handle grievances and feedback. +8. Handle grievances and feedback. 9. Provide reporting on key programme metrics. -HOPE is a digital product under the ownership of UNICE Programme Group Social Policy Social Protection. HOPE software libraries are release open source (https://github.com/unicef/hct-mis) the vision for HOPE is to become a Digital Public Good. +HOPE is a digital product under the ownership of UNICE Programme Group Social Policy Social Protection. HOPE software libraries are release open source (https://github.com/unicef/hct-mis) the vision for HOPE is to become a Digital Public Good. Picture1 Figure 1.1 The HOPE ecosystem HOPE enhances the quality of programmes implementing Humanitarian Cash Transfers (HCT) by increasing compliance with UNICEF guidance and tools and ensuring accountability and traceability of the information managed. KoBoToolbox was chosen for its widespread use in the humanitarian community as the main mobile digital data collection tool to be integrated with HOPE. HOPE guides users in processing the required data for each step of the programme cycle in a standardised manner to ensure programme quality and prevent implementation bottlenecks. -Following the Principles for Digital Development, the “reuse and improve” and “collaborative” concepts, HOPE integrates existing solutions used by the humanitarian community and contributes to their improvement, aiming at further developing these solutions as a public good. Use of RapidPro as an integrated monitoring solution in HOPE allows the system to engage in real time communication with beneficiaries and match their responses with their assistance records to verify that payments were received. +Following the Principles for Digital Development, the “reuse and improve” and “collaborative” concepts, HOPE integrates existing solutions used by the humanitarian community and contributes to their improvement, aiming at further developing these solutions as a public good. Use of RapidPro as an integrated monitoring solution in HOPE allows the system to engage in real time communication with beneficiaries and match their responses with their assistance records to verify that payments were received. ### Why -* Put cash recipients at the center with secure personal data processing +* Put cash recipients at the center with secure personal data processing * Unified consolidated reporting for beneficiary data * Increase accountability beneficiaries and donors * Financial Inclusion of Beneficiaries @@ -38,3 +38,20 @@ Following the Principles for Digital Development, the “reuse and improve” an * Ensuring due diligence in-cash transfer * Grow the use of effective cash program for children in a risk informed manner * Eliminate redundancies and dupes in how we do things + +## Legal +Humanitarian cash Operations and Programme Ecosystem +Copyright (c) 2014 - 2024 UNICEF + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . From eadd15f81d9a34f165745bc9a7d368f8358de337 Mon Sep 17 00:00:00 2001 From: Patryk Dabrowski Date: Tue, 9 Jul 2024 10:23:31 +0200 Subject: [PATCH 2/3] Fix create tp with not existing hh ids (#4002) --- .../hct_mis_api/apps/targeting/mutations.py | 4 +- ..._test_create_target_population_mutation.py | 102 ++++++++++++++++++ .../test_create_target_population_mutation.py | 5 + .../tests/test_targeting_validators.py | 76 +++++++++---- .../hct_mis_api/apps/targeting/validators.py | 20 +++- 5 files changed, 182 insertions(+), 25 deletions(-) diff --git a/backend/hct_mis_api/apps/targeting/mutations.py b/backend/hct_mis_api/apps/targeting/mutations.py index bd0e0c5f01..dd1a08d259 100644 --- a/backend/hct_mis_api/apps/targeting/mutations.py +++ b/backend/hct_mis_api/apps/targeting/mutations.py @@ -159,7 +159,7 @@ def processed_mutate(cls, root: Any, info: Any, **kwargs: Any) -> "CreateTargetP targeting_criteria_input = input_data.get("targeting_criteria") business_area = BusinessArea.objects.get(slug=input_data.pop("business_area_slug")) - TargetingCriteriaInputValidator.validate(targeting_criteria_input, program.data_collecting_type) + TargetingCriteriaInputValidator.validate(targeting_criteria_input, program) targeting_criteria = from_input_to_targeting_criteria(targeting_criteria_input, program) target_population = TargetPopulation( name=tp_name, @@ -251,7 +251,7 @@ def processed_mutate(cls, root: Any, info: Any, **kwargs: Any) -> "UpdateTargetP if targeting_criteria_input: should_rebuild_list = True - TargetingCriteriaInputValidator.validate(targeting_criteria_input, program.data_collecting_type) + TargetingCriteriaInputValidator.validate(targeting_criteria_input, program) targeting_criteria = from_input_to_targeting_criteria(targeting_criteria_input, program) if target_population.status == TargetPopulation.STATUS_OPEN: if target_population.targeting_criteria: diff --git a/backend/hct_mis_api/apps/targeting/tests/snapshots/snap_test_create_target_population_mutation.py b/backend/hct_mis_api/apps/targeting/tests/snapshots/snap_test_create_target_population_mutation.py index 19faf61c31..5a33e3d4a9 100644 --- a/backend/hct_mis_api/apps/targeting/tests/snapshots/snap_test_create_target_population_mutation.py +++ b/backend/hct_mis_api/apps/targeting/tests/snapshots/snap_test_create_target_population_mutation.py @@ -144,6 +144,108 @@ } } +snapshots['TestCreateTargetPopulationMutation::test_create_mutation_target_by_id 5'] = { + 'data': { + 'createTargetPopulation': { + 'targetPopulation': { + 'hasEmptyCriteria': True, + 'hasEmptyIdsCriteria': False, + 'name': 'Test name 5', + 'status': 'OPEN', + 'targetingCriteria': { + 'householdIds': '', + 'individualIds': 'IND-33', + 'rules': [ + ] + }, + 'totalHouseholdsCount': None, + 'totalIndividualsCount': None + } + } + } +} + +snapshots['TestCreateTargetPopulationMutation::test_create_mutation_target_by_id 6'] = { + 'data': { + 'createTargetPopulation': None + }, + 'errors': [ + { + 'locations': [ + { + 'column': 7, + 'line': 3 + } + ], + 'message': "['The given individuals do not exist in the current program']", + 'path': [ + 'createTargetPopulation' + ] + } + ] +} + +snapshots['TestCreateTargetPopulationMutation::test_create_mutation_target_by_id 7'] = { + 'data': { + 'createTargetPopulation': { + 'targetPopulation': { + 'hasEmptyCriteria': True, + 'hasEmptyIdsCriteria': False, + 'name': 'Test name 7', + 'status': 'OPEN', + 'targetingCriteria': { + 'householdIds': 'HH-1', + 'individualIds': '', + 'rules': [ + ] + }, + 'totalHouseholdsCount': None, + 'totalIndividualsCount': None + } + } + } +} + +snapshots['TestCreateTargetPopulationMutation::test_create_mutation_target_by_id 8'] = { + 'data': { + 'createTargetPopulation': None + }, + 'errors': [ + { + 'locations': [ + { + 'column': 7, + 'line': 3 + } + ], + 'message': "['The given households do not exist in the current program']", + 'path': [ + 'createTargetPopulation' + ] + } + ] +} + +snapshots['TestCreateTargetPopulationMutation::test_create_mutation_target_by_id 9'] = { + 'data': { + 'createTargetPopulation': None + }, + 'errors': [ + { + 'locations': [ + { + 'column': 7, + 'line': 3 + } + ], + 'message': "['There should be at least 1 rule in target criteria']", + 'path': [ + 'createTargetPopulation' + ] + } + ] +} + snapshots['TestCreateTargetPopulationMutation::test_create_mutation_with_comparison_method_contains_0_with_permission 1'] = { 'data': { 'createTargetPopulation': None diff --git a/backend/hct_mis_api/apps/targeting/tests/test_create_target_population_mutation.py b/backend/hct_mis_api/apps/targeting/tests/test_create_target_population_mutation.py index a635b08c56..49aaa4f2ec 100644 --- a/backend/hct_mis_api/apps/targeting/tests/test_create_target_population_mutation.py +++ b/backend/hct_mis_api/apps/targeting/tests/test_create_target_population_mutation.py @@ -256,6 +256,11 @@ def test_create_mutation_target_by_id(self) -> None: {"householdIds": "HH-1, HH-2, HH-3, ", "individualIds": "IND-33, IND-33, ", "rules": []}, {"householdIds": "HH-1", "individualIds": "IND-33", "rules": []}, {"householdIds": "", "individualIds": "IND-33", "rules": []}, + {"householdIds": "", "individualIds": "IND-33, IND-666", "rules": []}, + {"householdIds": "", "individualIds": "IND-666", "rules": []}, + {"householdIds": "HH-1, HH-666", "individualIds": "", "rules": []}, + {"householdIds": "HH-666", "individualIds": "", "rules": []}, + {"householdIds": "", "individualIds": "", "rules": []}, ] for num, targeting_criteria in enumerate(targeting_criteria_list, 1): diff --git a/backend/hct_mis_api/apps/targeting/tests/test_targeting_validators.py b/backend/hct_mis_api/apps/targeting/tests/test_targeting_validators.py index c90335fa13..d8be7b3838 100644 --- a/backend/hct_mis_api/apps/targeting/tests/test_targeting_validators.py +++ b/backend/hct_mis_api/apps/targeting/tests/test_targeting_validators.py @@ -1,54 +1,86 @@ from django.core.exceptions import ValidationError from django.test import TestCase -from hct_mis_api.apps.core.fixtures import DataCollectingTypeFactory +from hct_mis_api.apps.core.fixtures import DataCollectingTypeFactory, create_afghanistan from hct_mis_api.apps.core.models import DataCollectingType +from hct_mis_api.apps.household.fixtures import create_household +from hct_mis_api.apps.household.models import Household, Individual +from hct_mis_api.apps.program.fixtures import ProgramFactory +from hct_mis_api.apps.program.models import Program from hct_mis_api.apps.targeting.validators import TargetingCriteriaInputValidator class TestTargetingCriteriaInputValidator(TestCase): @classmethod def setUpTestData(cls) -> None: - cls.dct_standard = DataCollectingTypeFactory( - type=DataCollectingType.Type.STANDARD, - individual_filters_available=True, - household_filters_available=True, + create_afghanistan() + cls.program_standard = ProgramFactory( + data_collecting_type=DataCollectingTypeFactory( + type=DataCollectingType.Type.STANDARD, + individual_filters_available=True, + household_filters_available=True, + ) ) - cls.dct_standard_hh_only = DataCollectingTypeFactory( - type=DataCollectingType.Type.STANDARD, - individual_filters_available=False, - household_filters_available=True, + cls.program_standard_hh_only = ProgramFactory( + data_collecting_type=DataCollectingTypeFactory( + type=DataCollectingType.Type.STANDARD, + individual_filters_available=False, + household_filters_available=True, + ) ) - cls.dct_standard_ind_only = DataCollectingTypeFactory( - type=DataCollectingType.Type.STANDARD, - individual_filters_available=True, - household_filters_available=False, + cls.program_standard_ind_only = ProgramFactory( + data_collecting_type=DataCollectingTypeFactory( + type=DataCollectingType.Type.STANDARD, + individual_filters_available=True, + household_filters_available=False, + ) ) - cls.dct_social = DataCollectingTypeFactory( - type=DataCollectingType.Type.SOCIAL, - individual_filters_available=True, - household_filters_available=False, + cls.program_social = ProgramFactory( + data_collecting_type=DataCollectingTypeFactory( + type=DataCollectingType.Type.SOCIAL, + individual_filters_available=True, + household_filters_available=False, + ) ) def test_TargetingCriteriaInputValidator(self) -> None: validator = TargetingCriteriaInputValidator + create_household({"unicef_id": "HH-1", "size": 1}, {"unicef_id": "IND-1"}) with self.assertRaisesMessage( ValidationError, "Target criteria can has only filters or ids, not possible to has both" ): + self._update_program(self.program_standard) validator.validate( - {"rules": ["Rule1"], "household_ids": "HH-1", "individual_ids": "IND-1"}, self.dct_standard + {"rules": ["Rule1"], "household_ids": "HH-1", "individual_ids": "IND-1"}, self.program_standard ) with self.assertRaisesMessage(ValidationError, "Target criteria can has only individual ids"): - validator.validate({"rules": [], "household_ids": "HH-1", "individual_ids": "IND-1"}, self.dct_social) + self._update_program(self.program_social) + validator.validate({"rules": [], "household_ids": "HH-1", "individual_ids": "IND-1"}, self.program_social) + + self._update_program(self.program_standard_ind_only) validator.validate( - {"rules": [], "household_ids": "HH-1", "individual_ids": "IND-1"}, self.dct_standard_ind_only + {"rules": [], "household_ids": "HH-1", "individual_ids": "IND-1"}, self.program_standard_ind_only ) with self.assertRaisesMessage(ValidationError, "Target criteria can has only household ids"): + self._update_program(self.program_standard_hh_only) validator.validate( - {"rules": [], "household_ids": "HH-1", "individual_ids": "IND-1"}, self.dct_standard_hh_only + {"rules": [], "household_ids": "HH-1", "individual_ids": "IND-1"}, self.program_standard_hh_only ) with self.assertRaisesMessage(ValidationError, "There should be at least 1 rule in target criteria"): - validator.validate({"rules": [], "household_ids": "", "individual_ids": ""}, self.dct_standard_hh_only) + self._update_program(self.program_standard_hh_only) + validator.validate({"rules": [], "household_ids": "", "individual_ids": ""}, self.program_standard_hh_only) + + with self.assertRaisesMessage(ValidationError, "The given households do not exist in the current program"): + self._update_program(self.program_standard) + validator.validate({"rules": [], "household_ids": "HH-666", "individual_ids": ""}, self.program_standard) + + with self.assertRaisesMessage(ValidationError, "The given individuals do not exist in the current program"): + self._update_program(self.program_standard) + validator.validate({"rules": [], "household_ids": "", "individual_ids": "IND-666"}, self.program_standard) + + def _update_program(self, program: Program) -> None: + Household.objects.all().update(program=program) + Individual.objects.all().update(program=program) diff --git a/backend/hct_mis_api/apps/targeting/validators.py b/backend/hct_mis_api/apps/targeting/validators.py index 820446b5a9..c1f56eda09 100644 --- a/backend/hct_mis_api/apps/targeting/validators.py +++ b/backend/hct_mis_api/apps/targeting/validators.py @@ -8,6 +8,7 @@ from hct_mis_api.apps.core.models import DataCollectingType, FlexibleAttribute from hct_mis_api.apps.core.utils import get_attr_value from hct_mis_api.apps.core.validators import BaseValidator +from hct_mis_api.apps.household.models import Household, Individual from hct_mis_api.apps.program.models import Program from hct_mis_api.apps.targeting.models import ( TargetingCriteriaRuleFilter, @@ -135,7 +136,8 @@ def validate(rule: "Rule") -> None: class TargetingCriteriaInputValidator: @staticmethod - def validate(targeting_criteria: Dict, program_dct: DataCollectingType) -> None: + def validate(targeting_criteria: Dict, program: Program) -> None: + program_dct = program.data_collecting_type rules = targeting_criteria.get("rules", []) household_ids = targeting_criteria.get("household_ids") individual_ids = targeting_criteria.get("individual_ids") @@ -152,6 +154,22 @@ def validate(targeting_criteria: Dict, program_dct: DataCollectingType) -> None: logger.error("Target criteria can has only household ids") raise ValidationError("Target criteria can has only household ids") + if household_ids: + ids_list = household_ids.split(",") + ids_list = [i.strip() for i in ids_list] + ids_list = [i for i in ids_list if i.startswith("HH")] + if not Household.objects.filter(unicef_id__in=ids_list, program=program).exists(): + logger.error("The given households do not exist in the current program") + raise ValidationError("The given households do not exist in the current program") + + if individual_ids: + ids_list = individual_ids.split(",") + ids_list = [i.strip() for i in ids_list] + ids_list = [i for i in ids_list if i.startswith("IND")] + if not Individual.objects.filter(unicef_id__in=ids_list, program=program).exists(): + logger.error("The given individuals do not exist in the current program") + raise ValidationError("The given individuals do not exist in the current program") + if len(rules) < 1 and not household_ids and not individual_ids: logger.error("There should be at least 1 rule in target criteria") raise ValidationError("There should be at least 1 rule in target criteria") From b878822e1a17dee912bbd77a0c9da63e118b2b99 Mon Sep 17 00:00:00 2001 From: szymon-kellton <130459593+szymon-kellton@users.noreply.github.com> Date: Tue, 9 Jul 2024 10:31:55 +0200 Subject: [PATCH 3/3] Selenium: Payment Verification - happy path (#4008) * Init * Test * Added test_smoke_payment_verification_details * Black * Black * Black * Init * Added to tests payment record part * In progress test_happy_path_payment_verification * add data-cy for breadcrumbs * In progress test_happy_path_payment_verification * Added test_happy_path_payment_verification * update snaphots * Fix test_happy_path_payment_verification * Fix test_smoke_payment_verification_details * Black * Isort --------- Co-authored-by: Maciej Szewczyk --- backend/selenium_tests/conftest.py | 10 +- .../page_object/base_components.py | 8 ++ .../payment_verification/payment_record.py | 112 +++++++++++++++++ .../payment_verification_details.py | 13 ++ .../test_payment_verification.py | 118 +++++++++++++++--- .../core/BreadCrumbs/BreadCrumbs.tsx | 20 ++- frontend/src/components/core/PageHeader.tsx | 2 +- .../CreatePaymentPlanHeader.test.tsx.snap | 4 + .../CreateSetUpFspHeader.test.tsx.snap | 4 + .../__snapshots__/EditFspHeader.test.tsx.snap | 4 + .../EditPaymentPlanHeader.test.tsx.snap | 4 + .../__snapshots__/FspHeader.test.tsx.snap | 4 + .../CreatePaymentPlanHeader.test.tsx.snap | 4 + .../CreateSetUpFspHeader.test.tsx.snap | 4 + .../__snapshots__/EditFspHeader.test.tsx.snap | 4 + .../EditPaymentPlanHeader.test.tsx.snap | 4 + .../__snapshots__/FspHeader.test.tsx.snap | 4 + 17 files changed, 298 insertions(+), 25 deletions(-) create mode 100644 backend/selenium_tests/page_object/payment_verification/payment_record.py diff --git a/backend/selenium_tests/conftest.py b/backend/selenium_tests/conftest.py index 3ade3304ff..ac8fd8bef5 100644 --- a/backend/selenium_tests/conftest.py +++ b/backend/selenium_tests/conftest.py @@ -28,6 +28,7 @@ from page_object.payment_module.new_payment_plan import NewPaymentPlan from page_object.payment_module.payment_module import PaymentModule from page_object.payment_module.payment_module_details import PaymentModuleDetails +from page_object.payment_verification.payment_record import PaymentRecord from page_object.payment_verification.payment_verification import PaymentVerification from page_object.payment_verification.payment_verification_details import ( PaymentVerificationDetails, @@ -278,8 +279,8 @@ def pagePaymentModule(request: FixtureRequest, browser: Chrome) -> PaymentModule @pytest.fixture -def pagePaymentVerification(request: FixtureRequest, browser: Chrome) -> PaymentVerification: - yield PaymentVerification(browser) +def pagePaymentRecord(request: FixtureRequest, browser: Chrome) -> PaymentRecord: + yield PaymentRecord(browser) @pytest.fixture @@ -287,6 +288,11 @@ def pagePaymentVerificationDetails(request: FixtureRequest, browser: Chrome) -> yield PaymentVerificationDetails(browser) +@pytest.fixture +def pagePaymentVerification(request: FixtureRequest, browser: Chrome) -> PaymentVerification: + yield PaymentVerification(browser) + + @pytest.fixture def pageTargetingDetails(request: FixtureRequest, browser: Chrome) -> TargetingDetails: yield TargetingDetails(browser) diff --git a/backend/selenium_tests/page_object/base_components.py b/backend/selenium_tests/page_object/base_components.py index d4c554f01c..e71199903f 100644 --- a/backend/selenium_tests/page_object/base_components.py +++ b/backend/selenium_tests/page_object/base_components.py @@ -45,6 +45,8 @@ class BaseComponents(Common): globalProgramFilterSearchButton = 'button[data-cy="search-icon"]' globalProgramFilterClearButton = 'button[data-cy="clear-icon"]' rows = 'tr[role="checkbox"]' + breadcrumbsChevronIcon = 'svg[data-cy="breadcrumbs-chevron-icon"]' + arrowBack = 'div[data-cy="arrow_back"]' # Text globalProgramFilterText = "All Programmes" @@ -170,6 +172,12 @@ def getGlobalProgramFilterSearchButton(self) -> WebElement: def getGlobalProgramFilterSearchInput(self) -> WebElement: return self.wait_for(self.globalProgramFilterSearchInput) + def getBreadcrumbsChevronIcon(self) -> WebElement: + return self.wait_for(self.breadcrumbsChevronIcon) + + def getArrowBack(self) -> WebElement: + return self.wait_for(self.arrowBack) + def getNavProgramLog(self) -> WebElement: return self.wait_for(self.navProgramLog) diff --git a/backend/selenium_tests/page_object/payment_verification/payment_record.py b/backend/selenium_tests/page_object/payment_verification/payment_record.py new file mode 100644 index 0000000000..13dfa8dccb --- /dev/null +++ b/backend/selenium_tests/page_object/payment_verification/payment_record.py @@ -0,0 +1,112 @@ +from page_object.base_components import BaseComponents +from selenium.webdriver.remote.webelement import WebElement + + +class PaymentRecord(BaseComponents): + pageHeaderContainer = 'div[data-cy="page-header-container"]' + pageHeaderTitle = 'h5[data-cy="page-header-title"]' + buttonEdPlan = 'button[data-cy="button-ed-plan"]' + labelStatus = 'div[data-cy="label-STATUS"]' + statusContainer = 'div[data-cy="status-container"]' + labelHousehold = 'div[data-cy="label-Household"]' + labelTargetPopulation = 'div[data-cy="label-TARGET POPULATION"]' + labelDistributionModality = 'div[data-cy="label-DISTRIBUTION MODALITY"]' + labelAmountReceived = 'div[data-cy="label-AMOUNT RECEIVED"]' + labelHouseholdId = 'div[data-cy="label-HOUSEHOLD ID"]' + labelHeadOfHousehold = 'div[data-cy="label-HEAD OF HOUSEHOLD"]' + labelTotalPersonCovered = 'div[data-cy="label-TOTAL PERSON COVERED"]' + labelPhoneNumber = 'div[data-cy="label-PHONE NUMBER"]' + labelAltPhoneNumber = 'div[data-cy="label-ALT. PHONE NUMBER"]' + labelEntitlementQuantity = 'div[data-cy="label-ENTITLEMENT QUANTITY"]' + labelDeliveredQuantity = 'div[data-cy="label-DELIVERED QUANTITY"]' + labelCurrency = 'div[data-cy="label-CURRENCY"]' + labelDeliveryType = 'div[data-cy="label-DELIVERY TYPE"]' + labelDeliveryDate = 'div[data-cy="label-DELIVERY DATE"]' + labelEntitlementCardId = 'div[data-cy="label-ENTITLEMENT CARD ID"]' + labelTransactionReferenceId = 'div[data-cy="label-TRANSACTION REFERENCE ID"]' + labelEntitlementCardIssueDate = 'div[data-cy="label-ENTITLEMENT CARD ISSUE DATE"]' + labelFsp = 'div[data-cy="label-FSP"]' + buttonSubmit = 'button[data-cy="button-submit"]' + inputReceivedamount = 'input[data-cy="input-receivedAmount"]' + + def getInputReceivedamount(self) -> WebElement: + return self.wait_for(self.inputReceivedamount) + + def getPageHeaderContainer(self) -> WebElement: + return self.wait_for(self.pageHeaderContainer) + + def getPageHeaderTitle(self) -> WebElement: + return self.wait_for(self.pageHeaderTitle) + + def getButtonEdPlan(self) -> WebElement: + # Workaround because elements overlapped even though Selenium saw that they were available: + self.driver.execute_script( + """ + container = document.querySelector("div[data-cy='main-content']") + container.scrollBy(0,-200) + """ + ) + return self.wait_for(self.buttonEdPlan) + + def getLabelStatus(self) -> [WebElement]: + return self.get_elements(self.labelStatus) + + def getStatusContainer(self) -> WebElement: + return self.wait_for(self.statusContainer) + + def getLabelHousehold(self) -> WebElement: + return self.wait_for(self.labelHousehold) + + def getLabelTargetPopulation(self) -> WebElement: + return self.wait_for(self.labelTargetPopulation) + + def getLabelDistributionModality(self) -> WebElement: + return self.wait_for(self.labelDistributionModality) + + def getLabelAmountReceived(self) -> WebElement: + return self.wait_for(self.labelAmountReceived) + + def getLabelHouseholdId(self) -> WebElement: + return self.wait_for(self.labelHouseholdId) + + def getLabelHeadOfHousehold(self) -> WebElement: + return self.wait_for(self.labelHeadOfHousehold) + + def getLabelTotalPersonCovered(self) -> WebElement: + return self.wait_for(self.labelTotalPersonCovered) + + def getLabelPhoneNumber(self) -> WebElement: + return self.wait_for(self.labelPhoneNumber) + + def getLabelAltPhoneNumber(self) -> WebElement: + return self.wait_for(self.labelAltPhoneNumber) + + def getLabelEntitlementQuantity(self) -> WebElement: + return self.wait_for(self.labelEntitlementQuantity) + + def getLabelDeliveredQuantity(self) -> WebElement: + return self.wait_for(self.labelDeliveredQuantity) + + def getLabelCurrency(self) -> WebElement: + return self.wait_for(self.labelCurrency) + + def getLabelDeliveryType(self) -> WebElement: + return self.wait_for(self.labelDeliveryType) + + def getLabelDeliveryDate(self) -> WebElement: + return self.wait_for(self.labelDeliveryDate) + + def getLabelEntitlementCardId(self) -> WebElement: + return self.wait_for(self.labelEntitlementCardId) + + def getLabelTransactionReferenceId(self) -> WebElement: + return self.wait_for(self.labelTransactionReferenceId) + + def getLabelEntitlementCardIssueDate(self) -> WebElement: + return self.wait_for(self.labelEntitlementCardIssueDate) + + def getLabelFsp(self) -> WebElement: + return self.wait_for(self.labelFsp) + + def getButtonSubmit(self) -> WebElement: + return self.wait_for(self.buttonSubmit) diff --git a/backend/selenium_tests/page_object/payment_verification/payment_verification_details.py b/backend/selenium_tests/page_object/payment_verification/payment_verification_details.py index bcfbf796ad..b06090c372 100644 --- a/backend/selenium_tests/page_object/payment_verification/payment_verification_details.py +++ b/backend/selenium_tests/page_object/payment_verification/payment_verification_details.py @@ -40,6 +40,9 @@ class PaymentVerificationDetails(BaseComponents): labelStatusDiv = 'div[data-cy="label-STATUS"]' labelActivationDateDiv = 'div[data-cy="label-ACTIVATION DATE"]' labelCompletionDateDiv = 'div[data-cy="label-COMPLETION DATE"]' + buttonSubmit = 'button[data-cy="button-submit"]' + buttonFinish = 'button[data-cy="button-ed-plan"]' + rows = 'tr[role="checkbox"]' def getPageHeaderContainer(self) -> WebElement: return self.wait_for(self.pageHeaderContainer) @@ -148,3 +151,13 @@ def getLabelCompletionDateDiv(self) -> WebElement: def getLabelStatusDiv(self) -> WebElement: return self.wait_for(self.labelStatusDiv) + + def getButtonSubmit(self) -> WebElement: + return self.wait_for(self.buttonSubmit) + + def getButtonFinish(self) -> WebElement: + return self.wait_for(self.buttonFinish) + + def getRows(self) -> [WebElement]: + self.wait_for(self.rows) + return self.get_elements(self.rows) diff --git a/backend/selenium_tests/payment_verification/test_payment_verification.py b/backend/selenium_tests/payment_verification/test_payment_verification.py index 2f5866895a..d6d6b9a4b0 100644 --- a/backend/selenium_tests/payment_verification/test_payment_verification.py +++ b/backend/selenium_tests/payment_verification/test_payment_verification.py @@ -1,11 +1,14 @@ from datetime import datetime +from time import sleep import pytest from dateutil.relativedelta import relativedelta +from page_object.payment_verification.payment_record import PaymentRecord from page_object.payment_verification.payment_verification import PaymentVerification from page_object.payment_verification.payment_verification_details import ( PaymentVerificationDetails, ) +from selenium.webdriver.common.by import By from hct_mis_api.apps.account.models import User from hct_mis_api.apps.core.fixtures import DataCollectingTypeFactory @@ -18,6 +21,8 @@ PaymentVerificationFactory, PaymentVerificationPlanFactory, ) +from hct_mis_api.apps.payment.models import GenericPayment +from hct_mis_api.apps.payment.models import PaymentRecord as PR from hct_mis_api.apps.payment.models import PaymentVerification as PV from hct_mis_api.apps.payment.models import PaymentVerificationPlan from hct_mis_api.apps.program.fixtures import ProgramFactory @@ -71,7 +76,10 @@ def add_payment_verification() -> PV: name="TEST", program=program, business_area=BusinessArea.objects.first(), + start_date=datetime.now() - relativedelta(months=1), + end_date=datetime.now() + relativedelta(months=1), ) + targeting_criteria = TargetingCriteriaFactory() target_population = TargetPopulationFactory( @@ -79,7 +87,6 @@ def add_payment_verification() -> PV: targeting_criteria=targeting_criteria, business_area=BusinessArea.objects.first(), ) - payment_record = PaymentRecordFactory( parent=cash_plan, household=household, @@ -88,16 +95,23 @@ def add_payment_verification() -> PV: entitlement_quantity="21.36", delivered_quantity="21.36", currency="PLN", + status=GenericPayment.STATUS_DISTRIBUTION_SUCCESS, ) payment_verification_plan = PaymentVerificationPlanFactory( - generic_fk_obj=cash_plan, verification_channel=PaymentVerificationPlan.VERIFICATION_CHANNEL_MANUAL + generic_fk_obj=cash_plan, + verification_channel=PaymentVerificationPlan.VERIFICATION_CHANNEL_MANUAL, ) - return PaymentVerificationFactory( + pv_summary = cash_plan.get_payment_verification_summary + pv_summary.activation_date = datetime.now() - relativedelta(months=1) + pv_summary.save() + + pv = PaymentVerificationFactory( generic_fk_obj=payment_record, payment_verification_plan=payment_verification_plan, status=PV.STATUS_PENDING, ) + return pv @pytest.mark.usefixtures("login") @@ -140,11 +154,12 @@ def test_smoke_payment_verification_details( assert "0%" in pagePaymentVerificationDetails.getLabelErroneous().text assert "PENDING" in pagePaymentVerificationDetails.getLabelStatus().text assert "PENDING" in pagePaymentVerificationDetails.getVerificationPlansSummaryStatus().text + activation_date = (datetime.now() - relativedelta(months=1)).strftime("%-d %b %Y") assert ( - "ACTIVATION DATE -" + f"ACTIVATION DATE {activation_date}" in pagePaymentVerificationDetails.getLabelizedFieldContainerSummaryActivationDate().text.replace("\n", " ") ) - assert "-" in pagePaymentVerificationDetails.getLabelActivationDate().text + assert activation_date in pagePaymentVerificationDetails.getLabelActivationDate().text assert ( "COMPLETION DATE -" in pagePaymentVerificationDetails.getLabelizedFieldContainerSummaryCompletionDate().text.replace("\n", " ") @@ -161,27 +176,96 @@ def test_smoke_payment_verification_details( assert "PENDING" in pagePaymentVerificationDetails.getLabelStatus().text assert "PENDING" in pagePaymentVerificationDetails.getVerificationPlanStatus().text assert "MANUAL" in pagePaymentVerificationDetails.getLabelVerificationChannel().text - assert "-" in pagePaymentVerificationDetails.getLabelActivationDate().text + assert ( + str((datetime.now() - relativedelta(months=1)).strftime("%-d %b %Y")) + in pagePaymentVerificationDetails.getLabelActivationDate().text + ) assert "-" in pagePaymentVerificationDetails.getLabelCompletionDate().text - @pytest.mark.skip(reason="Do during the task: 198121") - def test_smoke_payment_verification_happy_path( + def test_happy_path_payment_verification( self, active_program: Program, add_payment_verification: PV, pagePaymentVerification: PaymentVerification, pagePaymentVerificationDetails: PaymentVerificationDetails, + pagePaymentRecord: PaymentRecord, ) -> None: pagePaymentVerification.selectGlobalProgramFilter("Active Program").click() pagePaymentVerification.getNavPaymentVerification().click() pagePaymentVerification.getCashPlanTableRow().click() - assert "0" in pagePaymentVerificationDetails.getLabelPaymentRecords().text - assert "23 Feb 2025" in pagePaymentVerificationDetails.getLabelStartDate().text - assert "26 May 2025" in pagePaymentVerificationDetails.getLabelEndDate().text + assert "1" in pagePaymentVerificationDetails.getLabelPaymentRecords().text + assert (datetime.now() - relativedelta(months=1)).strftime( + "%-d %b %Y" + ) in pagePaymentVerificationDetails.getLabelStartDate().text + assert (datetime.now() + relativedelta(months=1)).strftime( + "%-d %b %Y" + ) in pagePaymentVerificationDetails.getLabelEndDate().text assert "Bank reconciliation" in pagePaymentVerificationDetails.getTableLabel().text - assert "Full list" in pagePaymentVerificationDetails.getLabelSampling().text - assert "55" in pagePaymentVerificationDetails.getLabelResponded().text - assert "8" in pagePaymentVerificationDetails.getLabelReceivedWithIssues().text - assert "29" in pagePaymentVerificationDetails.getLabelSampleSize().text - assert "37" in pagePaymentVerificationDetails.getLabelReceived().text - assert "3" in pagePaymentVerificationDetails.getLabelNotReceived().text + payment_verification = add_payment_verification.payment_verification_plan + assert ( + payment_verification.sampling.lower().replace("_", " ") + in pagePaymentVerificationDetails.getLabelSampling().text.lower() + ) + assert str(payment_verification.responded_count) in pagePaymentVerificationDetails.getLabelResponded().text + assert ( + str(payment_verification.received_with_problems_count) + in pagePaymentVerificationDetails.getLabelReceivedWithIssues().text + ) + assert str(payment_verification.sample_size) in pagePaymentVerificationDetails.getLabelSampleSize().text + assert str(payment_verification.received_count) in pagePaymentVerificationDetails.getLabelReceived().text + assert str(payment_verification.not_received_count) in pagePaymentVerificationDetails.getLabelNotReceived().text + pagePaymentVerificationDetails.getButtonDeletePlan().click() + pagePaymentVerificationDetails.getButtonSubmit().click() + try: + pagePaymentVerificationDetails.getButtonNewPlan().click() + except BaseException: + sleep(3) + pagePaymentVerificationDetails.getButtonNewPlan().click() + pagePaymentVerificationDetails.getButtonSubmit().click() + + pagePaymentVerificationDetails.getButtonActivatePlan().click() + pagePaymentVerificationDetails.getButtonSubmit().click() + + pagePaymentVerificationDetails.getRows()[0].find_elements(By.TAG_NAME, "a")[0].click() + payment_record = PR.objects.first() + assert "Payment Record" in pagePaymentRecord.getPageHeaderTitle().text + assert "VERIFY" in pagePaymentRecord.getButtonEdPlan().text + assert "DELIVERED FULLY" in pagePaymentRecord.getLabelStatus()[0].text + assert "DELIVERED FULLY" in pagePaymentRecord.getStatusContainer().text + assert payment_record.household.unicef_id in pagePaymentRecord.getLabelHousehold().text + assert payment_record.target_population.name in pagePaymentRecord.getLabelTargetPopulation().text + assert payment_record.distribution_modality in pagePaymentRecord.getLabelDistributionModality().text + assert payment_record.verification.status in pagePaymentRecord.getLabelStatus()[1].text + assert "PLN 0.00" in pagePaymentRecord.getLabelAmountReceived().text + assert payment_record.household.unicef_id in pagePaymentRecord.getLabelHouseholdId().text + assert "21.36" in pagePaymentRecord.getLabelEntitlementQuantity().text + assert "21.36" in pagePaymentRecord.getLabelDeliveredQuantity().text + assert "PLN" in pagePaymentRecord.getLabelCurrency().text + assert "-" in pagePaymentRecord.getLabelDeliveryType().text + assert payment_record.service_provider.full_name in pagePaymentRecord.getLabelFsp().text + + pagePaymentRecord.getButtonEdPlan().click() + + pagePaymentRecord.getInputReceivedamount().click() + pagePaymentRecord.getInputReceivedamount().send_keys("100") + pagePaymentRecord.getButtonSubmit().click() + + for _ in range(5): + if "RECEIVED WITH ISSUES" in pagePaymentRecord.getLabelStatus()[1].text: + break + sleep(1) + assert "RECEIVED WITH ISSUES" in pagePaymentRecord.getLabelStatus()[1].text + pagePaymentRecord.getArrowBack().click() + + pagePaymentVerificationDetails.getButtonFinish().click() + pagePaymentVerificationDetails.getButtonSubmit().click() + + pagePaymentVerificationDetails.screenshot("1") + assert "Payment Plan" in pagePaymentVerificationDetails.getPageHeaderTitle().text + assert "FINISHED" in pagePaymentVerificationDetails.getLabelStatus().text + assert "FINISHED" in pagePaymentVerificationDetails.getVerificationPlanStatus().text + assert "100%" in pagePaymentVerificationDetails.getLabelSuccessful().text + + pagePaymentRecord.getArrowBack().click() + + assert "FINISHED" in pagePaymentVerification.getCashPlanTableRow().text diff --git a/frontend/src/components/core/BreadCrumbs/BreadCrumbs.tsx b/frontend/src/components/core/BreadCrumbs/BreadCrumbs.tsx index 3ff653bca5..86f62b4627 100644 --- a/frontend/src/components/core/BreadCrumbs/BreadCrumbs.tsx +++ b/frontend/src/components/core/BreadCrumbs/BreadCrumbs.tsx @@ -40,15 +40,22 @@ function BreadCrumbsElement({ onClick = () => null, }: BreadCrumbsElementProps): React.ReactElement { return ( - + {to ? ( - {title} + + {title} + ) : ( - + {title} )} - {!last ? : null} + {!last ? ( + + ) : null} ); } @@ -74,8 +81,11 @@ export function BreadCrumbs({ to={item.to} onClick={item.handleClick} last={last} + data-cy={`breadcrumbs-element-${index}`} /> ); }); - return {breadCrumbsElements}; + return ( + {breadCrumbsElements} + ); } diff --git a/frontend/src/components/core/PageHeader.tsx b/frontend/src/components/core/PageHeader.tsx index 59bc7f283a..c148e0abf0 100644 --- a/frontend/src/components/core/PageHeader.tsx +++ b/frontend/src/components/core/PageHeader.tsx @@ -84,7 +84,7 @@ export function PageHeader({ {breadCrumbs && breadCrumbs.length !== 0 ? ( // Leaving breadcrumbs for permissions, but BackButton goes back to the previous page - (handleBack ? handleBack() : window.history.back())} > diff --git a/frontend/src/components/paymentmodule/CreatePaymentPlan/CreatePaymentPlanHeader/__snapshots__/CreatePaymentPlanHeader.test.tsx.snap b/frontend/src/components/paymentmodule/CreatePaymentPlan/CreatePaymentPlanHeader/__snapshots__/CreatePaymentPlanHeader.test.tsx.snap index 3728f6561e..b1f57dbe83 100644 --- a/frontend/src/components/paymentmodule/CreatePaymentPlan/CreatePaymentPlanHeader/__snapshots__/CreatePaymentPlanHeader.test.tsx.snap +++ b/frontend/src/components/paymentmodule/CreatePaymentPlan/CreatePaymentPlanHeader/__snapshots__/CreatePaymentPlanHeader.test.tsx.snap @@ -11,6 +11,7 @@ exports[`components/paymentmodule/CreatePaymentPlanHeader should render 1`] = ` >
Payment Module diff --git a/frontend/src/components/paymentmodule/CreateSetUpFsp/CreateSetUpFspHeader/__snapshots__/CreateSetUpFspHeader.test.tsx.snap b/frontend/src/components/paymentmodule/CreateSetUpFsp/CreateSetUpFspHeader/__snapshots__/CreateSetUpFspHeader.test.tsx.snap index 1e89a32368..a470a084e2 100644 --- a/frontend/src/components/paymentmodule/CreateSetUpFsp/CreateSetUpFspHeader/__snapshots__/CreateSetUpFspHeader.test.tsx.snap +++ b/frontend/src/components/paymentmodule/CreateSetUpFsp/CreateSetUpFspHeader/__snapshots__/CreateSetUpFspHeader.test.tsx.snap @@ -11,6 +11,7 @@ exports[`components/paymentmodule/CreateSetUpFsp/CreateSetUpFspHeader should ren >
Payment Module diff --git a/frontend/src/components/paymentmodule/EditFsp/EditFspHeader/__snapshots__/EditFspHeader.test.tsx.snap b/frontend/src/components/paymentmodule/EditFsp/EditFspHeader/__snapshots__/EditFspHeader.test.tsx.snap index 2765a47825..d65d9b6a4e 100644 --- a/frontend/src/components/paymentmodule/EditFsp/EditFspHeader/__snapshots__/EditFspHeader.test.tsx.snap +++ b/frontend/src/components/paymentmodule/EditFsp/EditFspHeader/__snapshots__/EditFspHeader.test.tsx.snap @@ -11,6 +11,7 @@ exports[`components/paymentmodule/EditFspHeader should render 1`] = ` >
Payment Module diff --git a/frontend/src/components/paymentmodule/EditPaymentPlan/EditPaymentPlanHeader/__snapshots__/EditPaymentPlanHeader.test.tsx.snap b/frontend/src/components/paymentmodule/EditPaymentPlan/EditPaymentPlanHeader/__snapshots__/EditPaymentPlanHeader.test.tsx.snap index 05d08e0289..8e61f466ed 100644 --- a/frontend/src/components/paymentmodule/EditPaymentPlan/EditPaymentPlanHeader/__snapshots__/EditPaymentPlanHeader.test.tsx.snap +++ b/frontend/src/components/paymentmodule/EditPaymentPlan/EditPaymentPlanHeader/__snapshots__/EditPaymentPlanHeader.test.tsx.snap @@ -11,6 +11,7 @@ exports[`components/paymentmodule/EditPaymentPlanHeader should render 1`] = ` >
Payment Module diff --git a/frontend/src/components/paymentmodule/FspPlanDetails/FspHeader/__snapshots__/FspHeader.test.tsx.snap b/frontend/src/components/paymentmodule/FspPlanDetails/FspHeader/__snapshots__/FspHeader.test.tsx.snap index 527bfacab5..3107b9773a 100644 --- a/frontend/src/components/paymentmodule/FspPlanDetails/FspHeader/__snapshots__/FspHeader.test.tsx.snap +++ b/frontend/src/components/paymentmodule/FspPlanDetails/FspHeader/__snapshots__/FspHeader.test.tsx.snap @@ -11,6 +11,7 @@ exports[`components/paymentmodule/FspPlanDetails/FspHeader should render 1`] = ` >
Payment Module diff --git a/frontend/src/components/paymentmodulepeople/CreatePaymentPlan/CreatePaymentPlanHeader/__snapshots__/CreatePaymentPlanHeader.test.tsx.snap b/frontend/src/components/paymentmodulepeople/CreatePaymentPlan/CreatePaymentPlanHeader/__snapshots__/CreatePaymentPlanHeader.test.tsx.snap index 3728f6561e..b1f57dbe83 100644 --- a/frontend/src/components/paymentmodulepeople/CreatePaymentPlan/CreatePaymentPlanHeader/__snapshots__/CreatePaymentPlanHeader.test.tsx.snap +++ b/frontend/src/components/paymentmodulepeople/CreatePaymentPlan/CreatePaymentPlanHeader/__snapshots__/CreatePaymentPlanHeader.test.tsx.snap @@ -11,6 +11,7 @@ exports[`components/paymentmodule/CreatePaymentPlanHeader should render 1`] = ` >
Payment Module diff --git a/frontend/src/components/paymentmodulepeople/CreateSetUpFsp/CreateSetUpFspHeader/__snapshots__/CreateSetUpFspHeader.test.tsx.snap b/frontend/src/components/paymentmodulepeople/CreateSetUpFsp/CreateSetUpFspHeader/__snapshots__/CreateSetUpFspHeader.test.tsx.snap index 1e89a32368..a470a084e2 100644 --- a/frontend/src/components/paymentmodulepeople/CreateSetUpFsp/CreateSetUpFspHeader/__snapshots__/CreateSetUpFspHeader.test.tsx.snap +++ b/frontend/src/components/paymentmodulepeople/CreateSetUpFsp/CreateSetUpFspHeader/__snapshots__/CreateSetUpFspHeader.test.tsx.snap @@ -11,6 +11,7 @@ exports[`components/paymentmodule/CreateSetUpFsp/CreateSetUpFspHeader should ren >
Payment Module diff --git a/frontend/src/components/paymentmodulepeople/EditFsp/EditFspHeader/__snapshots__/EditFspHeader.test.tsx.snap b/frontend/src/components/paymentmodulepeople/EditFsp/EditFspHeader/__snapshots__/EditFspHeader.test.tsx.snap index 2765a47825..d65d9b6a4e 100644 --- a/frontend/src/components/paymentmodulepeople/EditFsp/EditFspHeader/__snapshots__/EditFspHeader.test.tsx.snap +++ b/frontend/src/components/paymentmodulepeople/EditFsp/EditFspHeader/__snapshots__/EditFspHeader.test.tsx.snap @@ -11,6 +11,7 @@ exports[`components/paymentmodule/EditFspHeader should render 1`] = ` >